<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Elliot lucky</title>
    <description>The latest articles on DEV Community by Elliot lucky (@codebigint_01).</description>
    <link>https://dev.to/codebigint_01</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1706891%2Feeaef3a0-80c9-4d44-9af3-3ae6d6d6f6a9.jpg</url>
      <title>DEV Community: Elliot lucky</title>
      <link>https://dev.to/codebigint_01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codebigint_01"/>
    <language>en</language>
    <item>
      <title>Shielded Token Contracts on Midnight: Real Errors, Real Fixes</title>
      <dc:creator>Elliot lucky</dc:creator>
      <pubDate>Wed, 01 Jul 2026 03:07:56 +0000</pubDate>
      <link>https://dev.to/codebigint_01/shielded-token-contracts-on-midnight-real-errors-real-fixes-4fc7</link>
      <guid>https://dev.to/codebigint_01/shielded-token-contracts-on-midnight-real-errors-real-fixes-4fc7</guid>
      <description>&lt;p&gt;&lt;em&gt;Written from months of grinding on shielded liquidity DeFi protocols on Midnight.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you've been trying to build anything serious with shielded fungible tokens on Midnight lending protocols, liquidity pools, DEXes you've probably hit some walls that the documentation doesn't fully prepare you for. The Midnight programming model around shielded tokens is genuinely different from anything in the EVM world, and a lot of the intuitions you carry from Solidity or even other ZK environments will get you into trouble fast.&lt;/p&gt;

&lt;p&gt;This post is a breakdown of the most impactful errors and misconceptions I ran into while building shielded liquidity DeFi contracts using Midnight's Compact language. These are not theoretical every single one of these either broke a circuit or caused a proof server failure at some point. I'll walk through what the issue is, why it happens, and what the correct pattern looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  Background: How Shielded Tokens Actually Work Under the Hood
&lt;/h2&gt;

&lt;p&gt;Before we get into the errors, let's get clear on the underlying mechanics because this context is what makes the errors make sense.&lt;/p&gt;

&lt;p&gt;Midnight uses a protocol called &lt;strong&gt;Zswap&lt;/strong&gt; for shielded token operations. When a user sends tokens to your contract by calling &lt;code&gt;receiveShielded&lt;/code&gt;, what actually happens is more involved than it looks on the surface.&lt;/p&gt;

&lt;p&gt;When your circuit calls &lt;code&gt;receiveShielded(coin)&lt;/code&gt;, the Compact runtime records a shielded receive obligation in the transaction being constructed. At this point, the proof server kicks in to generate the ZK proof for your circuit. But here's the thing your circuit only describes what the &lt;em&gt;contract side&lt;/em&gt; is doing. The transaction still needs to be &lt;em&gt;balanced&lt;/em&gt;: the tokens being received by the contract have to come from somewhere.&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;wallet&lt;/strong&gt; gets involved through an internal mechanism that runs beneath your circuit. The wallet looks at the &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; you're receiving the coin's color (token type) and value and finds a matching UTXO in the user's private coin set. It then generates a &lt;strong&gt;Zswap ownership proof&lt;/strong&gt; a ZK proof that proves the wallet owns a valid commitment in the global shielded ledger (via a nullifier), without revealing &lt;em&gt;which&lt;/em&gt; UTXO it is. This proof is what actually authorizes the spending of the user's tokens.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; that you receive in your circuit as a parameter is essentially the user's declaration: &lt;em&gt;"I have a coin of this color and this value that I'm sending you."&lt;/em&gt; The wallet is the one that provides the actual cryptographic evidence backing that claim. The proof server then bundles your circuit's proof together with the wallet's Zswap proof into a single transaction that satisfies all the balance constraints.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; has to arrive as a circuit parameter it has to exist &lt;em&gt;before&lt;/em&gt; the proof is constructed so the wallet knows what to balance against. You cannot dynamically request coins inside a circuit in a way the wallet didn't know about from the start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The coin comes in as a circuit parameter - the wallet uses this
// to find the matching UTXO and generate its Zswap ownership proof
export circuit deposit(incomingCoin_: ShieldedCoinInfo): [] {
    const incomingCoin = disclose(incomingCoin_);
    // At this point, the wallet has already committed to providing
    // a UTXO matching incomingCoin.color and incomingCoin.value
    receiveShielded(incomingCoin);
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that foundation in place, let's get into the errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error 1: Multiple Separate Shielded Balance Mappings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt; Creating separate ledger fields for each type of shielded asset the contract holds, or trying to manage shielded balances across multiple independent &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; ledger fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// DON'T do this
export ledger assetABalance: QualifiedShieldedCoinInfo;
export ledger assetBBalance: QualifiedShieldedCoinInfo;
export ledger assetCBalance: QualifiedShieldedCoinInfo;
// ...and growing as you add more assets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it fails:&lt;/strong&gt; The moment you try to receive or send shielded tokens from multiple separate ledger fields in anything but the most trivial scenarios, you'll start hitting proof construction issues and ledger state inconsistencies. It also completely kills your extensibility you can't add a new supported asset without changing the contract's ledger schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correct pattern:&lt;/strong&gt; One &lt;code&gt;Map&amp;lt;Bytes&amp;lt;32&amp;gt;, QualifiedShieldedCoinInfo&amp;gt;&lt;/code&gt; keyed by coin color. That's your single source of truth for everything the contract holds in shielded form. Separate tracking for accounting, positions, pool states all of that can live in whatever structure you want (Maps, commitments, Merkle trees). But the actual &lt;em&gt;shielded custody&lt;/em&gt; is one map.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ONE mapping to manage all shielded holdings
export ledger contractShieldedBalance: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, QualifiedShieldedCoinInfo&amp;gt;;

// Separate ledgers for your protocol accounting - no shielded tokens here
export ledger userPositions: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, UserPosition&amp;gt;;
export ledger assetConfigs: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, AssetConfig&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; returned by operations like &lt;code&gt;insertCoin&lt;/code&gt; or &lt;code&gt;mergeCoinImmediate&lt;/code&gt; already encapsulates the UTXO pointer the contract needs to spend from. You want all of that living in one place, keyed by color, so any circuit that needs to receive or send a particular asset knows exactly where to look.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error 2: Receiving and Sending from the Same Contract Balance in One Circuit
&lt;/h2&gt;

&lt;p&gt;This one caused the most confusing errors. The symptom is a &lt;strong&gt;public mismatch error from the proof server&lt;/strong&gt; which is cryptic enough that you might chase it for a while before finding the root cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt; Writing a single circuit that both receives shielded tokens &lt;em&gt;into&lt;/em&gt; the contract balance (&lt;code&gt;receiveShielded&lt;/code&gt; then update your balance map) &lt;em&gt;and&lt;/em&gt; sends shielded tokens &lt;em&gt;from&lt;/em&gt; the same balance map (&lt;code&gt;sendShielded&lt;/code&gt;) for the same coin type.&lt;/p&gt;

&lt;p&gt;The intuition that leads you here is natural in a two-sided operation like a collateral deposit paired with a loan payout, you want to take in one token and release another in the same transaction. In a liquidity rebalance, you take in one asset and send another. Seems like one atomic operation. It isn't, at least not when both sides touch the same &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it fails:&lt;/strong&gt; When your circuit calls &lt;code&gt;receiveShielded&lt;/code&gt;, it marks that UTXO slot as modified in one direction. When it calls &lt;code&gt;sendShielded&lt;/code&gt; from the same balance map entry, the proof server tries to reconcile the public inputs/outputs for that UTXO across both operations in the same proof and the balance equations don't hold cleanly. This manifests as the public input mismatch error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correct pattern:&lt;/strong&gt; Split the operation into two circuits one for receiving, one for sending.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Phase 1: receive the incoming token, update contract balance, mark state as pending
export circuit actionReceive(incomingCoin_: ShieldedCoinInfo): [] {
    const incomingCoin = disclose(incomingCoin_);
    // ... validate, update accounting state ...

    receiveShielded(incomingCoin);

    const existing = contractShieldedBalance.member(incomingCoin.color)
        ? mergeCoinImmediate(contractShieldedBalance.lookup(incomingCoin.color), incomingCoin)
        : incomingCoin;

    contractShieldedBalance.insertCoin(
        incomingCoin.color,
        existing,
        right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
    );

    // Flag that the send side is pending
    userState.insert(userKey, { ...currentState, status: Status.sendPending });
}

// Phase 2: send from contract balance, clear pending state
export circuit actionSend(outgoingCoinColor_: Bytes&amp;lt;32&amp;gt;): [] {
    const outgoingCoinColor = disclose(outgoingCoinColor_);
    const state = userState.lookup(userKey);
    assert(state.status == Status.sendPending, "Send is not pending");

    // Only sending here - no receiveShielded in this circuit
    const balance = contractShieldedBalance.lookup(outgoingCoinColor);
    const result = sendShielded(balance, left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(ownPublicKey()), state.pendingAmount);

    if (result.change.is_some) {
        contractShieldedBalance.insertCoin(outgoingCoinColor, result.change.value, right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self()));
    } else {
        contractShieldedBalance.remove(outgoingCoinColor);
    }

    userState.insert(userKey, { ...state, status: Status.settled, pendingAmount: 0 });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two-phase pattern also gives you a cleaner state machine for your protocol. The pending status flags aren't just organizational they're the safety check that ensures the second circuit can only run after the first one completed successfully on-chain.&lt;/p&gt;

&lt;p&gt;There are two specific cases where you &lt;em&gt;can&lt;/em&gt; mix shielded operations in one circuit, and they work fine because they don't touch the same &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; on both sides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case A: &lt;code&gt;receiveShielded&lt;/code&gt; + &lt;code&gt;mintShieldedToken&lt;/code&gt;&lt;/strong&gt; You're receiving one type of token (into your balance map) and minting a &lt;em&gt;different&lt;/em&gt; token (LP tokens, receipt tokens, etc.) directly to the user. No conflict because mint creates a new UTXO that wasn't in the contract's balance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Works fine: receive principal token, mint a receipt token to user
export circuit depositAndMintReceipt(incomingCoin_: ShieldedCoinInfo): [] {
    const incomingCoin = disclose(incomingCoin_);

    receiveShielded(incomingCoin);

    const existing = contractShieldedBalance.member(incomingCoin.color)
        ? mergeCoinImmediate(contractShieldedBalance.lookup(incomingCoin.color), incomingCoin)
        : incomingCoin;

    contractShieldedBalance.insertCoin(
        incomingCoin.color,
        existing,
        right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
    );

    // Mint a receipt token of a completely different color to the user
    mintShieldedToken(
        receiptTokenDomainSeparator,
        incomingCoin.value,
        mintNonce(),
        left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(ownPublicKey())
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Case B: &lt;code&gt;receiveShielded&lt;/code&gt; + &lt;code&gt;sendImmediateShielded&lt;/code&gt;&lt;/strong&gt; You receive a token and immediately send it to the burn address (or anywhere else) &lt;em&gt;without&lt;/em&gt; going through the contract's managed balance map. The &lt;code&gt;sendImmediate&lt;/code&gt; family of operations routes tokens directly without touching the contract's stored &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt;, so there's no conflict. This is the correct pattern for burning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Works fine: receive a receipt token from user, burn it immediately
export circuit burnReceiptAndWithdraw(receiptCoin_: ShieldedCoinInfo, underlyingCoinColor_: Bytes&amp;lt;32&amp;gt;): [] {
    const receiptCoin = disclose(receiptCoin_);
    const underlyingColor = disclose(underlyingCoinColor_);
    const burnAddr = shieldedBurnAddress();

    receiveShielded(receiptCoin);
    sendImmediateShielded(receiptCoin, burnAddr, receiptCoin.value); // burn receipt token directly

    // Now send the underlying asset FROM the contract balance (different coin type = no conflict)
    const balance = contractShieldedBalance.lookup(underlyingColor);
    const withdrawAmount = calculateWithdrawable(receiptCoin.value);
    const result = sendShielded(balance, left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(ownPublicKey()), withdrawAmount);

    if (result.change.is_some) {
        contractShieldedBalance.insertCoin(underlyingColor, result.change.value, right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self()));
    } else {
        contractShieldedBalance.remove(underlyingColor);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Error 3: Treating Shielded Token Balance Like an Unshielded Balance
&lt;/h2&gt;

&lt;p&gt;If you're coming from building unshielded token contracts on Midnight, you might expect the contract runtime to track your shielded holdings automatically. It doesn't.&lt;/p&gt;

&lt;p&gt;With unshielded tokens, the runtime handles the accounting your contract just calls the right transfer functions and the ledger reflects what's there. Shielded tokens work completely differently. The contract has &lt;strong&gt;no automatic awareness&lt;/strong&gt; of what shielded UTXOs it holds. If you receive a shielded deposit and don't explicitly track it in a ledger field, that UTXO is effectively lost you can't spend it because you have no reference to it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; type is what gives you that reference. It's the pointer from your contract's ledger into the shielded UTXO set. Without it, you cannot call &lt;code&gt;sendShielded&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger contractShieldedBalance: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, QualifiedShieldedCoinInfo&amp;gt;;

export circuit receiveDeposit(incomingCoin_: ShieldedCoinInfo): [] {
    const incomingCoin = disclose(incomingCoin_);

    receiveShielded(incomingCoin);

    // Without this insertCoin call, the tokens are received but permanently unspendable
    const coinToStore = contractShieldedBalance.member(incomingCoin.color)
        ? mergeCoinImmediate(contractShieldedBalance.lookup(incomingCoin.color), incomingCoin)
        : incomingCoin;

    contractShieldedBalance.insertCoin(
        incomingCoin.color,
        coinToStore,
        right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;insertCoin&lt;/code&gt; call is what binds the received UTXO to your contract's ledger state. Skip it and you've essentially sent tokens into a black hole.&lt;/p&gt;




&lt;h2&gt;
  
  
  Best Practice 4: Always Receive ShieldedCoinInfo as a Circuit Parameter
&lt;/h2&gt;

&lt;p&gt;This one is less a confirmed error and more a pattern I've settled on firmly: always accept &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; as a &lt;strong&gt;circuit parameter&lt;/strong&gt; before calling &lt;code&gt;receiveShielded&lt;/code&gt;. Do not attempt to derive or build the coin info from other inputs inside the circuit body.&lt;/p&gt;

&lt;p&gt;The reason this matters comes down to how the proof system works. The wallet needs to see the &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; at the point of transaction construction so it can identify the matching UTXO from the user's private coin set and generate its Zswap ownership proof. The circuit parameters are what the wallet reads to know what to balance against. If the coin information isn't surfaced as a parameter, the wallet has no clean way to participate in the proof at the right time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correct pattern:&lt;/strong&gt; Accept &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; as a parameter, disclose it inside the circuit, then pass it to &lt;code&gt;receiveShielded&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The coin arrives as a parameter - the wallet can read it and balance accordingly
export circuit deposit(incomingCoin_: ShieldedCoinInfo): [] {
    const incomingCoin = disclose(incomingCoin_);
    receiveShielded(incomingCoin);
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend or CLI is responsible for constructing the &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; from the user's wallet (the wallet API exposes the user's available coins), then passing it as a parameter to the circuit call. The wallet then handles the UTXO selection and ownership proof on its side.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error 5: Storing Each Deposit as an Independent UTXO
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt; Instead of merging new deposits into a single &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; balance, someone might reach for a &lt;code&gt;Set&amp;lt;QualifiedShieldedCoinInfo&amp;gt;&lt;/code&gt; to accumulate UTXOs, thinking they can collect them all and deal with selection later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// DON'T do this - accumulating independent UTXOs in a set
export ledger assetUTXOs: Set&amp;lt;QualifiedShieldedCoinInfo&amp;gt;;

export circuit deposit(incomingCoin_: ShieldedCoinInfo): [] {
    const incomingCoin = disclose(incomingCoin_);

    receiveShielded(incomingCoin);

    // Each deposit added as its own independent UTXO entry
    assetUTXOs.insertCoin(incomingCoin);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this is a problem:&lt;/strong&gt; When the contract needs to send, &lt;code&gt;sendShielded&lt;/code&gt; takes a single &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; as input. If your liquidity is spread across a set of independent UTXOs, you now have to filter and sort through them to find entries that cover the amount you need, handle partial coverage across multiple entries, and manually merge before you can send anything. That logic is complex, brittle, and entirely avoidable. You end up writing significant off-chain UTXO selection code for a problem that doesn't need to exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correct pattern:&lt;/strong&gt; Always merge deposits into a single aggregated balance per coin color using &lt;code&gt;mergeCoinImmediate&lt;/code&gt;. One coin color = one &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; entry in your map. When you deposit, merge. When you send, the single entry has everything you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit deposit(incomingCoin_: ShieldedCoinInfo): [] {
    const incomingCoin = disclose(incomingCoin_);

    receiveShielded(incomingCoin);

    // Merge into existing balance, or initialize if first deposit of this type
    const aggregated = contractShieldedBalance.member(incomingCoin.color)
        ? mergeCoinImmediate(contractShieldedBalance.lookup(incomingCoin.color), incomingCoin)
        : incomingCoin;

    contractShieldedBalance.insertCoin(
        incomingCoin.color,
        aggregated,
        right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mergeCoinImmediate&lt;/code&gt; is provided by the Compact standard library exactly for this purpose combining multiple UTXOs of the same coin type into a single &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt;. Use it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error 6: Not Handling Change After Sending
&lt;/h2&gt;

&lt;p&gt;This one is subtle. When your contract calls &lt;code&gt;sendShielded&lt;/code&gt; to send tokens to a user, the operation doesn't automatically return the change to the contract. You have to handle it explicitly.&lt;/p&gt;

&lt;p&gt;Think of it like spending cash if you hand over a 100-unit note to pay for something that costs 70, you need to receive 30 back. If you don't explicitly handle that change, it evaporates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit withdraw(coinColor_: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;128&amp;gt;, receiver: ZswapCoinPublicKey): [] {
    const coinColor = disclose(coinColor_);
    const balance = contractShieldedBalance.lookup(coinColor);
    sendShielded(balance, left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(receiver), amount);
    // BUG: change is lost. contractShieldedBalance still points to a spent UTXO.
    // The next attempt to send from this color will fail.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The correct pattern:&lt;/strong&gt; &lt;code&gt;sendShielded&lt;/code&gt; returns a result that contains the change (if any). Check it. If there's change, put it back into your balance map. If the balance is fully spent, remove the entry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit withdraw(coinColor_: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;128&amp;gt;, receiver: ZswapCoinPublicKey): [] {
    const coinColor = disclose(coinColor_);
    assert(contractShieldedBalance.member(coinColor), "No balance for this coin type");
    assert(amount &amp;gt; 0, "Invalid send amount");

    const balance = contractShieldedBalance.lookup(coinColor);
    const result = sendShielded(
        balance,
        left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(receiver),
        amount
    );

    // Always handle the change
    if (result.change.is_some) {
        contractShieldedBalance.insertCoin(
            coinColor,
            result.change.value,
            right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
        );
    } else {
        contractShieldedBalance.remove(coinColor); // fully spent, clean up
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you skip this and your contract later tries to call &lt;code&gt;sendShielded&lt;/code&gt; on a coin color whose UTXO was already spent in a previous transaction, the transaction will fail because the UTXO no longer exists in the ledger.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together: The Pattern That Works
&lt;/h2&gt;

&lt;p&gt;Combining all of the above, here's the mental model for shielded token management that I now use in every contract:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State design:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One &lt;code&gt;Map&amp;lt;Bytes&amp;lt;32&amp;gt;, QualifiedShieldedCoinInfo&amp;gt;&lt;/code&gt; as the single source of truth for all shielded holdings&lt;/li&gt;
&lt;li&gt;All other accounting (user positions, asset configs, protocol state) in separate non-shielded ledger structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Receive pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; as a circuit parameter&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;receiveShielded&lt;/code&gt; on the disclosed coin&lt;/li&gt;
&lt;li&gt;Merge into the balance map with &lt;code&gt;mergeCoinImmediate&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Send pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lookup from the balance map&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;sendShielded&lt;/code&gt;, capture the result&lt;/li&gt;
&lt;li&gt;Handle change or remove the entry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Split circuits wherever both sides touch the same &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receive in one circuit, update state to pending&lt;/li&gt;
&lt;li&gt;Send in a separate circuit, verify pending state, execute, finalize&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Safe exceptions where you can combine in one circuit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receiving a token + minting a different token (LP tokens, receipt tokens)&lt;/li&gt;
&lt;li&gt;Receiving a token + &lt;code&gt;sendImmediate&lt;/code&gt; to burn it (doesn't touch the balance map)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Compact Standard Library &lt;code&gt;receiveShielded&lt;/code&gt;, &lt;code&gt;sendShielded&lt;/code&gt;, &lt;code&gt;sendImmediateShielded&lt;/code&gt;, &lt;code&gt;mintShieldedToken&lt;/code&gt;, &lt;code&gt;mergeCoinImmediate&lt;/code&gt;: &lt;a href="https://docs.midnight.network/develop/reference/compact" rel="noopener noreferrer"&gt;docs.midnight.network/develop/reference/compact&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Shielded token concepts and &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt;: &lt;a href="https://docs.midnight.network/develop/guides/shielded-tokens" rel="noopener noreferrer"&gt;docs.midnight.network/develop/guides/shielded-tokens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zswap protocol overview: &lt;a href="https://docs.midnight.network/learn/zswap" rel="noopener noreferrer"&gt;docs.midnight.network/learn/zswap&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Transaction balancing and proof construction: &lt;a href="https://docs.midnight.network/develop/guides/transactions" rel="noopener noreferrer"&gt;docs.midnight.network/develop/guides/transactions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;These patterns took real trial and error to arrive at. The proof server errors in particular are notoriously unhelpful when you're deep in the wrong abstraction a "public mismatch" message doesn't tell you that you're trying to receive and send from the same balance in one circuit. Hopefully this saves someone else those hours.&lt;/p&gt;

&lt;p&gt;If you're building shielded DeFi on Midnight and running into something I didn't cover here, feel free to reach out.&lt;/p&gt;

&lt;p&gt;Follow Me on X: &lt;a href="https://x.com/codebigint_01" rel="noopener noreferrer"&gt;https://x.com/codebigint_01&lt;/a&gt;&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>blockchain</category>
      <category>defi</category>
      <category>web3</category>
    </item>
    <item>
      <title>Build a Private Vault DApp Smart Contract on Midnight with Compact</title>
      <dc:creator>Elliot lucky</dc:creator>
      <pubDate>Mon, 27 Apr 2026 01:25:37 +0000</pubDate>
      <link>https://dev.to/codebigint_01/build-a-private-vault-dapp-smart-contract-on-midnight-with-compact-53od</link>
      <guid>https://dev.to/codebigint_01/build-a-private-vault-dapp-smart-contract-on-midnight-with-compact-53od</guid>
      <description>&lt;p&gt;In this guide, we will build a vault dApp smart contract from scratch using Compact on Midnight.&lt;/p&gt;

&lt;p&gt;The goal is to help developers who already know the basics of Compact start writing a complete smart contract that handles real vault behavior: receiving shielded tokens, tracking user vault balances, accumulating multiple deposits, and withdrawing funds.&lt;/p&gt;

&lt;p&gt;We will focus on the contract side of the dApp. There is no frontend in this guide. The point is to understand the smart contract mechanics clearly before adding UI complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;We are building a shielded token vault contract where a user can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deposit a shielded coin into the vault&lt;/li&gt;
&lt;li&gt;automatically create a vault on the first deposit&lt;/li&gt;
&lt;li&gt;keep later deposits accumulated in the same vault&lt;/li&gt;
&lt;li&gt;reject deposits with the wrong coin color&lt;/li&gt;
&lt;li&gt;withdraw from the vault&lt;/li&gt;
&lt;li&gt;keep the contract's shielded UTXO balance updated manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://github.com/codeBigInt/midnight-simplified-tutorial-dapps/tree/main/vault-dapp" rel="noopener noreferrer"&gt;final project&lt;/a&gt; has this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vault-dapp/
|-- package.json
|-- src/
|   |-- vault.compact
|   |-- witness.ts
|   |-- managed/
|   `-- test/
|       |-- vault.test.ts
|       |-- vault-setup.ts
|       `-- utils.ts
`-- tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core files are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/vault.compact&lt;/code&gt;: the Compact smart contract&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/witness.ts&lt;/code&gt;: the TypeScript witness implementation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/test/vault-setup.ts&lt;/code&gt;: the simulator setup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/test/vault.test.ts&lt;/code&gt;: the behavior tests&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;To follow along, you should already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your Midnight developer environment set up&lt;/li&gt;
&lt;li&gt;the Compact toolchain installed&lt;/li&gt;
&lt;li&gt;basic Compact knowledge&lt;/li&gt;
&lt;li&gt;Bun installed&lt;/li&gt;
&lt;li&gt;a code editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure your Compact compiler version matches the language version used in this project. This contract was built against Compact compiler &lt;code&gt;0.31.0&lt;/code&gt; and language version &lt;code&gt;0.23.0&lt;/code&gt;, so the pragma is pinned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version 0.23.0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning the pragma avoids drift when newer compiler versions introduce language changes.&lt;/p&gt;

&lt;p&gt;You will also want the &lt;a href="https://docs.midnight.network/compact/standard-library/exports#receiveshielded" rel="noopener noreferrer"&gt;Compact standard library&lt;/a&gt; and &lt;a href="https://docs.midnight.network/compact/data-types/ledger-adt" rel="noopener noreferrer"&gt;Ledger data type&lt;/a&gt; documentation open because we be will using the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports#receiveshielded" rel="noopener noreferrer"&gt;&lt;code&gt;receiveShielded&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports#sendshielded" rel="noopener noreferrer"&gt;&lt;code&gt;sendShielded&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports#mergecoinimmediate" rel="noopener noreferrer"&gt;&lt;code&gt;mergeCoinImmediate&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/data-types/ledger-adt#insertcoin-1" rel="noopener noreferrer"&gt;&lt;code&gt;insertCoin&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports#ownpublickey" rel="noopener noreferrer"&gt;&lt;code&gt;ownPublicKey&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/data-types/ledger-adt#self" rel="noopener noreferrer"&gt;&lt;code&gt;kernel.self&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Project
&lt;/h2&gt;

&lt;p&gt;Create the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;vault-dapp
&lt;span class="nb"&gt;cd &lt;/span&gt;vault-dapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize the project with Bun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose the blank template. Bun will create the initial &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then create the source folder and the Compact file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;src
&lt;span class="nb"&gt;touch &lt;/span&gt;src/vault.compact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Add Compile Scripts
&lt;/h2&gt;

&lt;p&gt;Before writing the full contract, add scripts so you do not have to keep typing the Compact compiler command manually.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;package.json&lt;/code&gt;, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test-compile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"compact compile --skip-zk src/vault.compact ./src/managed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"compact compile src/vault.compact ./src/managed"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;test-compile&lt;/code&gt; script compiles the Compact code without generating the full zero-knowledge materials. That makes it useful while you are actively developing.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;compile&lt;/code&gt; script performs the full compile.&lt;/p&gt;

&lt;p&gt;Both scripts write generated artifacts into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/managed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that folder does not exist, the compiler creates it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Start the Compact Contract
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;src/vault.compact&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first line is the pinned language pragma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version 0.23.0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import the standard library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import CompactStandardLibrary;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standard library gives us access to the built-in circuits and types we need for token transfer, ledger helpers, hashing, and shielded coin management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add the Main Circuit Skeleton
&lt;/h2&gt;

&lt;p&gt;The vault has two exported circuits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit deposit(
    _coin: ShieldedCoinInfo
): []{
}

export circuit withdraw(_amount: Uint&amp;lt;128&amp;gt;): []{
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the user-facing actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deposit&lt;/code&gt;: deposit shielded tokens into the contract, creating the user's vault on the first deposit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;withdraw&lt;/code&gt;: withdraw shielded tokens from the contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run a quick compile to confirm the setup works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run test-compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the setup is correct, Compact generates the managed TypeScript artifacts in &lt;code&gt;src/managed&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Define the Vault Struct
&lt;/h2&gt;

&lt;p&gt;Now we define the shape of a vault position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export struct Vault {
    balance: Uint&amp;lt;128&amp;gt;;
    coinColor: Bytes&amp;lt;32&amp;gt;;
    ownerHash: Bytes&amp;lt;32&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vault stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;balance&lt;/code&gt;: the amount deposited into the vault&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;coinColor&lt;/code&gt;: the token color this vault accepts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ownerHash&lt;/code&gt;: a commitment to the public key that owns the vault&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Midnight, token types are identified by a byte string called a color. Different assets have different colors, so we use &lt;code&gt;coinColor&lt;/code&gt; to make sure a user keeps one vault position tied to one asset type.&lt;/p&gt;

&lt;p&gt;The final code uses &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt; for &lt;code&gt;balance&lt;/code&gt; because &lt;code&gt;ShieldedCoinInfo.value&lt;/code&gt; is also &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt;. If you use &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; for &lt;code&gt;balance&lt;/code&gt;, the compiler will complain when you add a coin value to the vault balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy Trade-Off
&lt;/h2&gt;

&lt;p&gt;This tutorial keeps the contract intentionally small, but the design has an important privacy trade-off.&lt;/p&gt;

&lt;p&gt;The shielded coin itself is private, and token movement uses &lt;code&gt;receiveShielded&lt;/code&gt; and &lt;code&gt;sendShielded&lt;/code&gt;. However, each &lt;code&gt;Vault.balance&lt;/code&gt; is stored as a plain &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt; in a public &lt;code&gt;Map&lt;/code&gt;, and each vault also stores the accepted &lt;code&gt;coinColor&lt;/code&gt;. That means the contract publicly exposes each vault's running accounting balance and asset color, even though the underlying coin is shielded.&lt;/p&gt;

&lt;p&gt;This is acceptable for a learning example focused on shielded token handling and UTXO management. A production privacy-first vault should usually keep per-user balances in private state and store only commitments on-chain, for example with a Merkle-tree based design that proves balance updates without exposing each user's balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Add Ledger State
&lt;/h2&gt;

&lt;p&gt;We need one ledger map for vault positions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger vaults: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Vault&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maps a generated user ID to a vault.&lt;/p&gt;

&lt;p&gt;We also need another map to manage shielded coin balances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger balances: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, QualifiedShieldedCoinInfo&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This map stores contract-held shielded coins by coin color.&lt;/p&gt;

&lt;p&gt;This part is important: when you deal with shielded tokens, the contract does not automatically maintain an easy balance table for you. You receive the shielded coin, but you still need to manage the resulting UTXO state yourself. That is why the &lt;code&gt;balances&lt;/code&gt; map exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  ShieldedCoinInfo vs QualifiedShieldedCoinInfo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports#shieldedcoininfo" rel="noopener noreferrer"&gt;&lt;code&gt;ShieldedCoinInfo&lt;/code&gt;&lt;/a&gt; represents a newly created shielded coin used when receiving or spending a coin in a transaction. It has fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nonce&lt;/li&gt;
&lt;li&gt;color&lt;/li&gt;
&lt;li&gt;value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports#qualifiedshieldedcoininfo" rel="noopener noreferrer"&gt;&lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt;&lt;/a&gt; represents a shielded coin that already exists on the ledger and is ready to be spent. It includes the extra information needed to locate it, such as the Merkle tree index.&lt;/p&gt;

&lt;p&gt;That is why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deposits accept &lt;code&gt;ShieldedCoinInfo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;contract balances are stored as &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;insertCoin&lt;/code&gt; converts the received shielded coin into the qualified form stored in the ledger&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: Add the Witness Function
&lt;/h2&gt;

&lt;p&gt;We do not want to store the user's raw secret in public ledger state.&lt;/p&gt;

&lt;p&gt;Instead, we derive a user ID from a secret key stored in the user's private state.&lt;/p&gt;

&lt;p&gt;Declare the witness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness getSecretKey(): Bytes&amp;lt;32&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getSecretKey()&lt;/code&gt; lets the contract receive the user's secret key from private state during execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Generate a User ID
&lt;/h2&gt;

&lt;p&gt;Now add a helper circuit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;circuit generateUserId(sk: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt;{
    return persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "vaul:user"),
        sk
    ]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;persistentHash&lt;/code&gt; gives us a deterministic ID from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a fixed domain separator&lt;/li&gt;
&lt;li&gt;the user's secret key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the commitment-based per-user identity used by the vault. It is the part of the design that lets the contract track balances by a commitment derived from private witness data instead of by storing a raw user address.&lt;/p&gt;

&lt;p&gt;The current code uses &lt;code&gt;"vault:user"&lt;/code&gt; as the domain separator. For a real deployed contract, choose this string carefully before deployment because changing it later changes all derived user IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Add the Owner Commitment Helper
&lt;/h2&gt;

&lt;p&gt;The owner commitment stores a hash of the public key used when the vault is first created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;circuit generateOwnerCommitment(pk: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt;{
    return persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "vault:owner:commitment"),
        pk
    ]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not meant to reveal the public key as a raw field in the vault. The vault stores only the commitment hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Implement deposit
&lt;/h2&gt;

&lt;p&gt;The user deposits a shielded coin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit deposit(
    _coin: ShieldedCoinInfo
): []{
    const coin = disclose(_coin);

    receiveShielded(coin);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;receiveShielded&lt;/code&gt; validates that the coin is being received by the contract in this transaction.&lt;/p&gt;

&lt;p&gt;But receiving is not enough. The contract still needs to store or update the shielded balance manually.&lt;/p&gt;

&lt;p&gt;If there is already a balance for this coin color, merge the previous ledger coin with the new coin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if(balances.member(coin.color)){
    const prevBalance = balances.lookup(coin.color);
    const newBalance = mergeCoinImmediate(prevBalance, coin);

    balances.insertCoin(
        coin.color,
        newBalance,
        right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
    );
}else{
    balances.insertCoin(
       coin.color,
       coin,
       right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;a href="https://docs.midnight.network/compact/standard-library/exports#mergecoinimmediate" rel="noopener noreferrer"&gt;&lt;code&gt;mergeCoinImmediate&lt;/code&gt;&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports#mergecoin" rel="noopener noreferrer"&gt;&lt;code&gt;mergeCoin&lt;/code&gt;&lt;/a&gt; is for merging two coins that already exist on the ledger. In this case, we are merging an existing &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; with a new incoming &lt;code&gt;ShieldedCoinInfo&lt;/code&gt;, so we use &lt;code&gt;mergeCoinImmediate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;The recipient type can be either a user public key or a contract address. Since the contract is storing this balance for itself, we pass the contract address using the &lt;a href="https://docs.midnight.network/compact/standard-library/exports#right" rel="noopener noreferrer"&gt;&lt;code&gt;right&lt;/code&gt;&lt;/a&gt; constructor.&lt;/p&gt;

&lt;p&gt;Next, derive the user ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const userId = generateUserId(disclose(getSecretKey()));
const hasVault = vaults.member(userId);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first deposit, create the vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if(!hasVault){
    const ownerCommitmentHash = generateOwnerCommitment(ownPublicKey().bytes);

    const newVault = Vault {
        balance: coin.value,
        coinColor: disclose(coin.color),
        ownerHash: ownerCommitmentHash
    };

    vaults.insert(userId, newVault);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On later deposits, require the same coin color and add the new amount to the existing balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;else{
    const vault = vaults.lookup(userId);

    assert (coin.color == vault.coinColor, "Invalid coin type deposited");

    const updatedVault = Vault {
        ...vault,
        balance: (vault.balance + coin.value) as Uint&amp;lt;128&amp;gt;
    };

    vaults.insert(userId, updatedVault);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cast to &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt; matters because &lt;code&gt;coin.value&lt;/code&gt; is &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run the compile script again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run test-compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 11: Implement withdraw
&lt;/h2&gt;

&lt;p&gt;The withdrawal circuit accepts an amount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit withdraw(_amount: Uint&amp;lt;128&amp;gt;): []{
    const amount = disclose(_amount);
    const userId = generateUserId(disclose(getSecretKey()));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reconstruct the owner commitment from the public key creating the transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const reconstructedOwnerHash = generateOwnerCommitment(ownPublicKey().bytes);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the user has a vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert (vaults.member(userId), "You have no vault position");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load the vault and check the user has enough balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const vault = vaults.lookup(userId);
assert (vault.balance &amp;gt;= amount, "Insufficient vault balance");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify that the current public key matches the owner commitment stored when the vault was created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert (reconstructedOwnerHash == vault.ownerHash, "Unauthorized: You are not the owner");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load the contract-held shielded coin for the vault's coin color:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const balanceToSendFrom = balances.lookup(vault.coinColor);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now call &lt;code&gt;sendShielded&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const sendResult = sendShielded(
    balanceToSendFrom,
    left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(ownPublicKey()),
    amount
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sendShielded&lt;/code&gt; sends a value from a shielded coin owned by the contract to a recipient.&lt;/p&gt;

&lt;p&gt;Here the recipient is the user creating the transaction, so we use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(ownPublicKey())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to use ownPublicKey()
&lt;/h3&gt;

&lt;p&gt;&lt;a href="(https://docs.midnight.network/compact/standard-library/exports#ownpublickey)"&gt;&lt;code&gt;ownPublicKey()&lt;/code&gt;&lt;/a&gt; is useful when a circuit needs the current transaction creator's public key as the target of an operation, for example sending a shielded coin to the caller.&lt;/p&gt;

&lt;p&gt;Do not treat &lt;code&gt;ownPublicKey()&lt;/code&gt; as a substitute for a verified private identity by itself. If a circuit authorizes access to private-account state, bind that access to private witness data or to a commitment established earlier in the contract. In this vault, the public key is committed into &lt;code&gt;ownerHash&lt;/code&gt; when the vault is first created, and withdrawal checks that same commitment before sending funds.&lt;/p&gt;

&lt;p&gt;After sending, Compact returns a send result. The important part for this contract is the change. If there is change left, we must store it. If there is no change, we remove the balance entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if(sendResult.change.is_some){
    balances.insertCoin(
        vault.coinColor,
        sendResult.change.value,
        right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
    );
}else{
    balances.remove(vault.coinColor);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reduce the user's vault balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const updatedVault = Vault {
    ...vault,
    balance: (vault.balance - amount) as Uint&amp;lt;128&amp;gt;
};

vaults.insert(userId, updatedVault);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the compile script again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run test-compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see an error around &lt;code&gt;ownPublicKey&lt;/code&gt;, check the capitalization. It should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ownPublicKey()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Full Contract
&lt;/h2&gt;

&lt;p&gt;Here is the complete &lt;code&gt;src/vault.compact&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Minimal Single Shielded Token Type Vault Dapp
 *
 * Key Concept demonstrated:
 *  Structured onchain pattern for contract state management involving Maps, Struct and hashes
 *  Shielded liquidity managment techniques using using receiveShielded and sendShielded
 *  Risk managment for private state compromise scenario using generateOwnerCommitment and ownerHash field for ownership verification
 *
 * NB: In this contract individual vault balance is publicly visible onchain which is a trade-off to keep the contract minimal as the focus
 * of this example is to demonstrate shielded token management. Usually individual shielded token balance should be kept in private state while commitments are stored onchain
 * utilzing ADT types like Merkle trees (completely shielded and verifiable by zk proof) or Maps 
 */
pragma language_version 0.23.0;
import CompactStandardLibrary;

export struct Vault {
    balance: Uint&amp;lt;128&amp;gt;;
    coinColor: Bytes&amp;lt;32&amp;gt;;
    ownerHash: Bytes&amp;lt;32&amp;gt;;
}

export ledger vaults: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Vault&amp;gt;;
export ledger balances: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, QualifiedShieldedCoinInfo&amp;gt;;

witness getSecretKey(): Bytes&amp;lt;32&amp;gt;;

export circuit deposit(
    _coin: ShieldedCoinInfo
): []{
    const coin = disclose(_coin);

    receiveShielded(coin);

    if(balances.member(coin.color)){
        const prevBalance = balances.lookup(coin.color);
        const newBalance = mergeCoinImmediate(prevBalance, coin);

        balances.insertCoin(
            coin.color,
            newBalance,
            right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
        );
    }else{
        balances.insertCoin(
           coin.color,
           coin,
           right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
        );
    }

    const userId = generateUserId(disclose(getSecretKey()));
    const hasVault = vaults.member(userId);

    if(!hasVault){
        const ownerCommitmentHash = generateOwnerCommitment(ownPublicKey().bytes);

        const newVault = Vault {
            balance: coin.value,
            coinColor: disclose(coin.color),
            ownerHash: ownerCommitmentHash
        };

        vaults.insert(userId, newVault);
    }else{
        const vault = vaults.lookup(userId);

        assert (coin.color == vault.coinColor, "Invalid coin type deposited");

        const updatedVault = Vault {
            ...vault,
            balance: (vault.balance + coin.value) as Uint&amp;lt;128&amp;gt;
        };

        vaults.insert(userId, updatedVault);
    }
}

export circuit withdraw(_amount: Uint&amp;lt;128&amp;gt;): []{
    const amount = disclose(_amount);
    const userId = generateUserId(disclose(getSecretKey()));

    const reconstructedOwnerHash = generateOwnerCommitment(ownPublicKey().bytes);

    assert (vaults.member(userId), "You have no vault position");

    const vault = vaults.lookup(userId);
    assert (vault.balance &amp;gt;= amount, "Insufficient vault balance");

    assert (reconstructedOwnerHash == vault.ownerHash, "Unauthorized: You are not the owner");

    const balanceToSendFrom = balances.lookup(vault.coinColor);

    const sendResult = sendShielded(
        balanceToSendFrom,
        left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(ownPublicKey()),
        amount
    );

    if(sendResult.change.is_some){
        balances.insertCoin(
            vault.coinColor,
            sendResult.change.value,
            right&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(kernel.self())
        );
    }else{
        balances.remove(vault.coinColor);
    }

    const updatedVault = Vault {
        ...vault,
        balance: (vault.balance - amount) as Uint&amp;lt;128&amp;gt;
    };

    vaults.insert(userId, updatedVault);
}

circuit generateUserId(sk: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt;{
    return persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "vaul:user"),
        sk
    ]);
}

circuit generateOwnerCommitment(pk: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt;{
    return persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "vault:owner:commitment"),
        pk
    ]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 12: Implement the TypeScript Witness
&lt;/h2&gt;

&lt;p&gt;The Compact file declares the witness. TypeScript provides the runtime implementation.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;src/witness.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;WitnessContext&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@midnight-ntwrk/compact-runtime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ledger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./managed/contract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;VaultPrivateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createVaultPrivateState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;VaultPrivateState&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;secretKey&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;witnesses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;getSecretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;privateState&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;WitnessContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ledger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VaultPrivateState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;VaultPrivateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;privateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The witness returns a tuple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[privateState, witnessValue]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this project, the private state does not change, so the witness returns the same private state it received.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 13: Test the Contract
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/codeBigInt/midnight-simplified-tutorial-dapps/tree/main/vault-dapp" rel="noopener noreferrer"&gt;final project&lt;/a&gt; includes Vitest tests around the generated contract bindings.&lt;/p&gt;

&lt;p&gt;The simulator in &lt;code&gt;src/test/vault-setup.ts&lt;/code&gt; deploys the generated &lt;code&gt;Contract&lt;/code&gt;, initializes private state, and exposes helper methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TEST_COIN_COLOR&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Ledger&lt;/span&gt;
&lt;span class="nf"&gt;withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Ledger&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploying with empty ledgers&lt;/li&gt;
&lt;li&gt;creating a vault on the first valid deposit&lt;/li&gt;
&lt;li&gt;accumulating two deposits into one vault balance&lt;/li&gt;
&lt;li&gt;rejecting invalid coin colors&lt;/li&gt;
&lt;li&gt;partial withdrawal&lt;/li&gt;
&lt;li&gt;full withdrawal and balance removal&lt;/li&gt;
&lt;li&gt;rejecting withdrawal when the witness secret does not match an existing vault&lt;/li&gt;
&lt;li&gt;rejecting withdrawal when a stolen witness secret is used from a different public key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The accumulation test is important. The core behavior is not only accepting deposits, but merging multiple deposits into one running vault balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;simulator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;simulator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getOnlyVault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;simulator&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run &lt;span class="nb"&gt;test&lt;/span&gt;:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test Files  1 passed (1)
Tests       9 passed (9)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What You Learned
&lt;/h2&gt;

&lt;p&gt;In this tutorial, you learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start a Compact project with Bun&lt;/li&gt;
&lt;li&gt;add fast and full compile scripts&lt;/li&gt;
&lt;li&gt;write exported Compact circuits&lt;/li&gt;
&lt;li&gt;define ledger maps&lt;/li&gt;
&lt;li&gt;model a vault position with a struct&lt;/li&gt;
&lt;li&gt;use witness functions for private state&lt;/li&gt;
&lt;li&gt;generate a commitment-based user ID&lt;/li&gt;
&lt;li&gt;receive shielded tokens with &lt;code&gt;receiveShielded&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;store shielded coin balances with &lt;code&gt;insertCoin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;merge new deposits with &lt;code&gt;mergeCoinImmediate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;send withdrawals with &lt;code&gt;sendShielded&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;manage returned change manually&lt;/li&gt;
&lt;li&gt;test the contract with generated TypeScript bindings&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Improvements
&lt;/h2&gt;

&lt;p&gt;This vault is intentionally minimal. If you want to extend it, try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adding zero-amount checks&lt;/li&gt;
&lt;li&gt;supporting multiple vaults per user&lt;/li&gt;
&lt;li&gt;extracting repeated balance update logic into helper circuits&lt;/li&gt;
&lt;li&gt;adding events or richer metadata&lt;/li&gt;
&lt;li&gt;building a frontend for deposit and withdraw actions&lt;/li&gt;
&lt;li&gt;supporting only one approved asset instead of multiple coin colors&lt;/li&gt;
&lt;li&gt;moving per-user balances into private state and storing only commitments on-chain&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;This vault dApp is a compact example of how Midnight smart contracts can combine private state, shielded assets, and public ledger updates.&lt;/p&gt;

&lt;p&gt;If you are learning Compact, this is a good project to study because it touches the pieces you need for many real dApps: identity derivation, token transfer, state management, and testing.&lt;/p&gt;

&lt;p&gt;GitHub link: &lt;a href="https://github.com/codeBigInt/midnight-simplified-tutorial-dapps/tree/main/vault-dapp" rel="noopener noreferrer"&gt;https://github.com/codeBigInt/midnight-simplified-tutorial-dapps/tree/main/vault-dapp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>privacy</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
    <item>
      <title>JOURNEY INTO BLOCKCHAIN DEVELOPMENT THE EASY WAY</title>
      <dc:creator>Elliot lucky</dc:creator>
      <pubDate>Sat, 01 Nov 2025 15:28:53 +0000</pubDate>
      <link>https://dev.to/codebigint_01/journey-into-blockchain-development-the-easy-way-40jj</link>
      <guid>https://dev.to/codebigint_01/journey-into-blockchain-development-the-easy-way-40jj</guid>
      <description>&lt;p&gt;Hmmm, thank goodness you found this blog post. I’m sure by now you’ve toured half the internet searching for an easier way to get started in the blockchain industry as a developer. I get it looks complicated, sounds technical, and can feel overwhelming trying to piece everything together.&lt;/p&gt;

&lt;p&gt;That’s exactly why I decided to share my experience to give you a simpler, clearer path into blockchain development and hopefully inspire your own journey.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Like Isaac Newton once said, “If I have seen further, it is by standing on the shoulders of giants.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So before we dive into my personal story, let’s first understand what blockchain really is, and why it was invented in plain, beginner-friendly terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 WHAT AND WHY BLOCKCHAIN?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s take a scenario.&lt;br&gt;
When you think of a bank, you think about monetary records of people who own accounts i.e., a ledger or journal, although a bit advanced these days with the invention of computers. These ledgers are owned and controlled by these banks. What happens if there’s a fire outbreak and all the financial records become historical ashes (not the cricket trophy, LOL 🤭)? I mean actual ashes.&lt;/p&gt;

&lt;p&gt;Of course, we would not know who owns what. This is one of the reasons blockchain was invented a “distributed/decentralized ledger,” not owned or controlled by a single entity or enterprise.&lt;/p&gt;

&lt;p&gt;These ledger records are distributed in the sense that they are stored on several computers around the globe called “nodes,” each having the same copy of the ledger.&lt;/p&gt;

&lt;p&gt;In today’s world where ledgers are no longer used, it can be likened to a distributed or decentralized database.&lt;/p&gt;

&lt;p&gt;But you may ask, why then is it called a blockchain and not just a decentralized database? Well, instead of storing user transactions or data in rows and columns as banks would do, transactions or data are bundled into small units called “blocks.” And unlike pages of a ledger linked together by glue and referencing each other in the head of the documents, each block is linked together by “hashes”; a fixed-length string that represents the data contained in the block and the next block combined together.&lt;/p&gt;

&lt;p&gt;Enough of the jargon. Back to my experience diving into blockchain development. There are different blockchains, each solving a particular problem e.g., Bitcoin (removes the need for financial intermediaries like banks), Ethereum (eliminating the need for third parties in agreements through smart contracts), Cardano, Midnight (a privacy-centered blockchain), Avalanche, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤯 MY EXPERIENCE DEVELOPING ON ETHEREUM
&lt;/h2&gt;

&lt;p&gt;For me, I started with Ethereum not because it was easy to learn, but because it was one of the most popular at the time and still is. Development on Ethereum introduced the concept of smart contracts, which is now an integral part of every blockchain.&lt;/p&gt;

&lt;p&gt;In fact, smart contracts are what make modern blockchains usable. They are “self-executing” programs or code you write and deploy to the blockchain just like you would on AWS or Vercel with an application you just finished building. They execute themselves when the conditions you define within the contract are met. Like any other application (web or mobile), they have to be coded using a programming language. For Ethereum, I had to learn “Solidity.”&lt;/p&gt;

&lt;p&gt;Solidity was difficult for me to catch up with, as it had a syntax (language writing pattern) that looked like C++ and JavaScript combined. Being a newbie in programming who only had knowledge of web development-centered languages like JavaScript, a bit of TypeScript (a subset of JavaScript), and frontend frameworks like React as at the time, I became overwhelmed.&lt;/p&gt;

&lt;p&gt;I had to learn how to manage memory usage while writing Solidity code, structs, maps, etc. After a while, I knew I was not just ready for it and had to go back to where I came from 😄, web development. Still with the intent of learning blockchain, but with a failed attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡WHAT CHANGED?
&lt;/h2&gt;

&lt;p&gt;Eventually, I came across a sidechain built on the Cardano blockchain called Midnight. Unlike every other blockchain, its language is so easy to learn, and it solves a key pain point in blockchain, decentralization without compromising users’ privacy. On most blockchains, everything is public, including user assets, details, and confidential information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://midnight.network/" rel="noopener noreferrer"&gt;&lt;br&gt;
Midnight blockchain&lt;/a&gt; solves this by ensuring users’ private data is secure and allowing them to choose when to share their info, while still maintaining the decentralized nature expected from every blockchain.&lt;/p&gt;

&lt;p&gt;Most importantly, its smart contract language is really easy to learn. It’s just like TypeScript. That’s to say, if you know TypeScript, you can dive straight in and start developing on Midnight. The language is called Minokawa (formerly Compact). It uses data types similar to arrays, objects, functions, and strings in TypeScript. Except that it differs slightly in how you write them.&lt;/p&gt;

&lt;p&gt;This was when I knew I had found the right spot for me to master and experience what developing on the blockchain feels like without having to learn an entirely strange programming language. Believe me, in less than 3 months I had learned how to write complex smart contracts just because I knew TypeScript. Its ecosystem is enriched with great minds building and collaborating on both individual projects and hackathons (two of which I have won) all in the quest to improve the ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT I RECOMMEND ⭐⭐⭐⭐⭐
&lt;/h2&gt;

&lt;p&gt;While choosing a blockchain to start developing on depends on the programming languages and concepts one is already familiar with, I would recommend &lt;a href="https://midnight.network/" rel="noopener noreferrer"&gt;Midnight Blockchain&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because if you’re someone like me coming from a web or mobile development background, you already know JavaScript. This means you can easily learn TypeScript in less than a month and transition into blockchain development on &lt;a href="https://docs.midnight.network/develop/reference/compact/writing" rel="noopener noreferrer"&gt;Midnight&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Everyone’s blockchain journey is different, but the key is to start where you are and build from what you already know.&lt;/p&gt;

&lt;p&gt;That’s what helped me find my path and hopefully, this helps you find yours too.&lt;/p&gt;

&lt;p&gt;Want to checkout Midnight? Here’s a glimpse of what it feels like writing smart contracts!&lt;br&gt;
&lt;a href="https://docs.midnight.network/develop/reference/compact/writing" rel="noopener noreferrer"&gt;https://docs.midnight.network/develop/reference/compact/writing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stay curious. Keep building. The future is decentralized. ✨&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>midnightchallenge</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
