Skip to main content

Private Transfer

Transferring tokens inside the shielded pool is completely private — amounts, senders, and receivers are hidden.

How it works​

import { transfer, waitForTransfer } from '@shroud/sdk';

const { tx, recipientNote, changeNote } = await transfer({
signer,
provider,
poolAddress: "0x91c912eac...",
inputNote: myNote, // note to spend
transferAmount: 50n, // amount to send
recipientPublicKey: bobPk, // Bob's Baby Jubjub public key
senderPublicKey: myPk,
senderPrivateKey: myPrivKey,
wasmPath: "/transfer.wasm",
zkeyPath: "/transfer_final.zkey",
});

const { recipient, change } = await waitForTransfer(tx, recipientNote, changeNote, provider, poolAddress);

Transfer flow​

  1. Select input note — choose an unspent note with sufficient balance
  2. Compute outputs:
    • Recipient note: amount = transferAmount
    • Change note: amount = inputNote.amount - transferAmount
    • Blinding conservation: changeBlinding = inputNote.blinding - recipientBlinding
  3. Generate random values for both output notes (secrets, nullifier preimages)
  4. Compute commitments — Pedersen + Poseidon for both outputs
  5. Sync Merkle tree — fetch events and reconstruct tree locally
  6. Generate Groth16 proof — snarkjs in browser (~1 second)
  7. Encrypt memos — ECDH + AES-256-GCM for each recipient
  8. Submit transaction — proof, nullifier, 2 new commitments, 2 encrypted memos

What the contract sees​

Only 4 public signals from the proof:

SignalContains
merkle_rootWhich tree state the proof is against
nullifier_hashPrevents double-spend (unlinkable to note without secret)
new_commitment_1Opaque hash — hides amount, recipient, everything
new_commitment_2Opaque hash — hides amount, recipient, everything

The contract verifies the Groth16 proof, marks the nullifier as spent, and inserts both new commitments. It learns nothing about amounts, senders, or receivers.

Relayed transfer​

Transfers can also be submitted via the relay API to avoid linking your EVM address to the transaction:

import { relayTransfer } from '@shroud/sdk';

const result = await relayTransfer({
provider,
poolAddress: "0x91c912eac...",
inputNote: myNote,
transferAmount: 50n,
recipientPublicKey: bobPk,
senderPublicKey: myPk,
senderPrivateKey: myPrivKey,
wasmPath: "/transfer.wasm",
zkeyPath: "/transfer_final.zkey",
relayUrl: "/api/relay",
paymasterAddress: "0x102C04f39...",
});

The relay wallet submits the transaction — your EVM address never appears on-chain.