pient.
// Only callable by authorized logic in production; simplified here for demonstration.
function issue(recipient: Address, amount: UInt64) {
require(amount > 0, "Issue amount must be positive");
state.total_supply += amount;
// Safe addition with fallback for new addresses
let current_balance = state.balances[recipient] ?? 0;
state.balances[recipient] = current_balance + amount;
}
// Transfers tokens between addresses.
function transfer(sender: Address, receiver: Address, amount: UInt64) {
require(amount > 0, "Transfer amount must be positive");
require(state.balances[sender] >= amount, "Insufficient sender balance");
state.balances[sender] -= amount;
let receiver_balance = state.balances[receiver] ?? 0;
state.balances[receiver] = receiver_balance + amount;
}
}
#### 2. Compilation and Artifact Generation
Compact code must be compiled to generate artifacts that the Midnight node and frontend can consume. The compilation process produces JSON representations of the contract interface, types, and circuit constraints.
**Command:**
```bash
compact compile contracts/TransparentAsset.compact contracts/managed/transparent-asset
Rationale: The managed/ directory contains the output artifacts. These files are consumed by the API bridge to serialize inputs and deserialize outputs. Never commit compiled artifacts to version control if they are generated dynamically; however, for stable releases, pinning artifacts ensures reproducibility.
3. API Bridge Implementation
Direct frontend interaction with the Midnight node is discouraged due to serialization complexity and security concerns. An API bridge handles the translation between HTTP requests and Compact function calls.
Architecture Decision: The bridge runs as a separate service. It loads the compiled artifacts, validates incoming payloads against Compact types, and submits transactions to the ledger. This decouples the UI from ledger specifics and allows for independent scaling of the API layer.
// scripts/ledger-proxy.mjs
import express from 'express';
import { loadCompactArtifacts } from '@midnight-ir/compact-runtime';
import { createLedgerClient } from './ledger-client.mjs';
const app = express();
app.use(express.json());
// Load artifacts generated by the compiler
const artifacts = loadCompactArtifacts('./contracts/managed/transparent-asset');
const ledger = createLedgerClient(process.env.MIDNIGHT_NODE_URL);
app.post('/api/issue', async (req, res) => {
try {
const { recipient, amount } = req.body;
// Validate types against Compact schema
const validated = artifacts.schemas.issue.validate({ recipient, amount });
const tx = await ledger.submitTransaction('issue', validated);
res.json({ status: 'submitted', txHash: tx.hash });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.get('/api/state/:address', async (req, res) => {
const { address } = req.params;
const state = await ledger.queryState('balances', address);
res.json({ balance: state ?? 0n });
});
app.listen(4000, () => console.log('Ledger proxy running on port 4000'));
4. Frontend Integration with React
The React application manages wallet state, user inputs, and balance displays. It communicates with the API bridge via fetch calls.
Architecture Decision: We use custom hooks to encapsulate wallet logic and data fetching. This promotes reusability and keeps components clean. BigInt is used for all numeric values to prevent precision loss, as JavaScript's Number type cannot safely represent large integers common in blockchain contexts.
// src/components/AssetManager.tsx
import { useState, useEffect, useCallback } from 'react';
import { useLaceWallet } from '../hooks/useLaceWallet';
import { fetchBalance, submitIssue } from '../services/api';
export const AssetManager = () => {
const { address, isConnected, connect } = useLaceWallet();
const [balance, setBalance] = useState<BigInt>(0n);
const [inputAmount, setInputAmount] = useState<string>('');
const [isProcessing, setIsProcessing] = useState(false);
const refreshBalance = useCallback(async () => {
if (!address) return;
const data = await fetchBalance(address);
setBalance(BigInt(data.balance));
}, [address]);
useEffect(() => {
refreshBalance();
}, [refreshBalance]);
const handleIssue = async () => {
if (!address || !inputAmount) return;
setIsProcessing(true);
try {
await submitIssue({
recipient: address,
amount: BigInt(inputAmount)
});
await refreshBalance();
setInputAmount('');
} catch (err) {
console.error('Issue failed:', err);
} finally {
setIsProcessing(false);
}
};
if (!isConnected) {
return <button onClick={connect}>Connect Lace Wallet</button>;
}
return (
<div className="asset-manager">
<h2>Transparent Asset Manager</h2>
<p>Address: {address}</p>
<p>Balance: {balance.toString()}</p>
<div className="controls">
<input
type="text"
value={inputAmount}
onChange={(e) => setInputAmount(e.target.value)}
placeholder="Amount"
/>
<button
onClick={handleIssue}
disabled={isProcessing || !inputAmount}
>
{isProcessing ? 'Processing...' : 'Issue Tokens'}
</button>
</div>
</div>
);
};
Pitfall Guide
Production experience with Midnight reveals specific failure modes. Avoid these common mistakes to ensure stability and correctness.
-
JavaScript Number Precision Loss
- Explanation: Using
Number for token amounts causes silent data corruption for values exceeding Number.MAX_SAFE_INTEGER.
- Fix: Enforce
BigInt for all balance and amount variables. Convert user input strings to BigInt explicitly before transmission.
-
Ignoring Compact Compilation Warnings
- Explanation: Compact may produce warnings about unused variables or type mismatches that do not halt compilation but cause runtime failures.
- Fix: Treat warnings as errors. Run
npm run doctor or equivalent validation scripts after every contract change. Inspect the managed/ output for completeness.
-
State Drift After Transactions
- Explanation: The frontend may display stale balances because the ledger state update is asynchronous and the UI does not poll or listen for events.
- Fix: Implement a refresh mechanism after transaction submission. For production, use WebSocket subscriptions or polling intervals to sync state.
-
Hardcoding Contract Addresses
- Explanation: Embedding contract addresses in frontend code breaks when deploying to different networks or upgrading contracts.
- Fix: Inject addresses via environment variables or a configuration service. Support dynamic address resolution based on the connected network.
-
Skipping Artifact Setup
- Explanation: Cloning a repository without running the setup script results in missing artifacts, causing the API bridge to crash on startup.
- Fix: Always run the setup command (e.g.,
npm run setup) after cloning. Verify artifact existence before starting services.
-
Confusing Unshielded and Shielded Models
- Explanation: Attempting to hide data in an unshielded contract leads to confusion, as the ledger will still expose the state.
- Fix: Clearly document the privacy model. If data must be private, switch to a shielded contract design. Do not mix models within the same contract without explicit separation.
-
API Rate Limiting and Throttling
- Explanation: Rapid UI interactions can flood the API bridge, causing transaction nonce conflicts or node overload.
- Fix: Implement debouncing on input fields. Add queue management in the API bridge to serialize transaction submissions per address.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Rapid Prototyping | Unshielded + Compact | Fast iteration with visible state reduces debugging time. | Low Dev Cost |
| Regulatory Audit | Unshielded | Transparent ledger allows auditors to verify flows directly. | Compliance Savings |
| High-Security Finance | Shielded | Privacy is mandatory; unshielded exposes sensitive data. | Higher Dev/Compute Cost |
| Educational Onboarding | Unshielded | Transparency helps developers understand state transitions. | Training Efficiency |
Configuration Template
Use this template to standardize project scripts and dependencies.
{
"name": "midnight-transparent-asset",
"version": "1.0.0",
"scripts": {
"setup": "npm install && npm run compile:contract",
"compile:contract": "compact compile contracts/TransparentAsset.compact contracts/managed/transparent-asset",
"dev:api": "node scripts/ledger-proxy.mjs",
"dev:ui": "vite",
"dev": "concurrently \"npm run dev:api\" \"npm run dev:ui\"",
"doctor": "node scripts/doctor.mjs",
"check:artifacts": "node scripts/check-artifacts.mjs"
},
"dependencies": {
"@midnight-ir/compact-runtime": "^latest",
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"concurrently": "^8.2.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}
Quick Start Guide
- Initialize Project: Clone the repository and run
npm run setup to install dependencies and compile the contract.
- Validate Environment: Execute
npm run doctor to ensure all tools are configured correctly.
- Start Services: Run
npm run dev to launch the API bridge and React frontend concurrently.
- Connect Wallet: Open the UI in a browser, click "Connect Lace Wallet," and authorize the connection.
- Test Flow: Enter an amount and click "Issue Tokens." Verify the balance updates and the transaction appears in the Lace Wallet history.