This tutorial walks through wrapping ERC20 tokens into Anoma shielded resources and executing an atomic swap between two parties — all on Ethereum Sepolia testnet with ZK proofs generated remotely via RISC Zero Bonsai.
The flow has four steps:
fund_wallet ──► wrap_erc20 (maker) ──► wrap_erc20 (taker) ──► swap_test
│ │ │ │
│ Mint testnet │ ERC20 → Anoma │ ERC20 → Anoma │ Atomic swap:
│ USDC & DAI │ resource (USDC) │ resource (DAI) │ USDC ↔ DAI
│ via Aave │ + ZK proof │ + ZK proof │ + 8 ZK proofs
│ Faucet │ + on-chain TX │ + on-chain TX │ + on-chain TX
What happens under the hood:
fund_wallet— Mints testnet USDC and DAI via the Aave V3 Faucet on Sepolia.wrap_erc20(x2) — For each token: approves Permit2, builds an Anoma resource (consumed ephemeral + created persistent), generates compliance and logic ZK proofs via Bonsai, and submits the wrap transaction to the Protocol Adapter.swap_test— Loads both wrap outputs, reconstructs the resources, builds cross-swap resources (maker's USDC → taker, taker's DAI → maker), generates 8 ZK proofs (2 compliance + 4 logic + 1 delta + 1 aggregation), and submits the atomic swap to the Protocol Adapter.
- Rust toolchain (
rustup,cargo) - A BIP-39 seed phrase with Sepolia ETH for gas (~0.1 ETH is plenty)
- API keys for Alchemy and RISC Zero Bonsai
The swap_app crate lives on the jonaprieto/swap-app branch:
git clone [email protected]:jonaprieto/anomapay-backend.git
cd anomapay-backend
git checkout jonaprieto/swap-appExport the required environment variables in your shell (e.g. add to ~/.zshrc):
export WALLET_SEED="forward truth repeat truly claw syrup blossom task session garden pull ill"
export BONSAI_API_URL="https://bento.heliax.fyi"
export BONSAI_API_KEY="149G32a+L/Fw734WeL0Mz1nJjkU/scpAKXpud+8oLtM="
export ALCHEMY_API_KEY="4APYD3IZW3XSVMHpebKQ-"
export RPC_URL="https://eth-sepolia.g.alchemy.com/v2/4APYD3IZW3XSVMHpebKQ-"
export GALILEO_INDEXER_ADDRESS="http://localhost:4000"Then reload: source ~/.zshrc
Note:
WALLET_SEEDis a BIP-39 mnemonic. The binaries derive the Ethereum wallet at index 0. Alternatively, setFEE_PAYMENT_WALLET_PRIVATE_KEYwith a hex-encoded private key.Important: The binaries read env vars via
std::env::var— there is no automatic.envfile loading. You mustexportthe variables in your shell.
cargo build -p swap_app \
--bin fund_wallet \
--bin wrap_erc20 \
--bin swap_testThis compiles three binaries in target/debug/:
| Binary | Purpose |
|---|---|
fund_wallet |
Mints testnet tokens via Aave Faucet |
wrap_erc20 |
Wraps an ERC20 token into an Anoma resource |
swap_test |
Executes an atomic swap between two wrapped resources |
Mint testnet USDC (6 decimals) and DAI (18 decimals) from the Aave V3 Faucet:
cargo run -p swap_app --bin fund_walletExpected output:
=== AnomaPay Testnet Token Funder ===
Wallet: 0xe13F26693A549Bf034968D7107d9dC7C81CffCBa
Chain ID: 11155111 (Sepolia = 11155111)
Minting 10 USDC (10000000 base units) via Aave Faucet...
TX: 0xc6baf8bd...
Minting 10 DAI (10000000000000000000 base units) via Aave Faucet...
TX: 0xaab95a54...
=== Token addresses for wrap_erc20 ===
USDC (Aave): 0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8
DAI (Aave): 0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357
Note the Aave token addresses — these are different from Circle's native Sepolia USDC. Use these addresses in the wrap steps.
Wrap 5 USDC into an Anoma resource:
ERC20_TOKEN="0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8" \
WRAP_AMOUNT_WEI=5000000 \
WRAP_OUTPUT=wrap_maker.json \
cargo run -p swap_app --bin wrap_erc20This performs the following sequence:
- Derives wallet from seed phrase
- Checks token balance and Permit2 approval
- Builds a resource pair: consumed ephemeral (tokens entering the system) + created persistent (the shielded Anoma resource)
- Signs a Permit2 witness transfer
- Generates ZK proofs via Bonsai (compliance + logic proofs)
- Submits the transaction to the Protocol Adapter on Sepolia
- Saves keychain + resource metadata to
wrap_maker.json
Expected output:
=== AnomaPay ERC20 Wrap Tool ===
Wallet address: 0xe13F26693A549Bf034968D7107d9dC7C81CffCBa
Token: 0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8
Amount: 5000000 wei
Created resource commitment: 0xccfe43b3...
Generating ZK proofs via Bonsai (this may take a few minutes)...
Transaction generated and verified!
=== SUCCESS ===
Sepolia TX hash: 0x95283570...
Wrap output saved to: wrap_maker.json
Wrap 5 DAI into a second Anoma resource:
ERC20_TOKEN="0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357" \
WRAP_AMOUNT_WEI=5000000000000000000 \
WRAP_OUTPUT=wrap_taker.json \
cargo run -p swap_app --bin wrap_erc20Note: DAI has 18 decimals, so 5 DAI =
5000000000000000000wei. USDC has 6 decimals, so 5 USDC =5000000wei.
With both wrap_maker.json and wrap_taker.json in place, run the swap:
MAKER_WRAP_OUTPUT=wrap_maker.json \
TAKER_WRAP_OUTPUT=wrap_taker.json \
cargo run -p swap_app --bin swap_testThe swap binary:
- Loads both wrap output JSON files
- Reconstructs the persistent resources and verifies commitments match
- Builds cross-swap resources: maker's USDC → taker, taker's DAI → maker
- Computes the action tree (Merkle tree of nullifiers and commitments)
- Signs the action tree root with both parties' authorization keys
- Generates 8 ZK proofs via Bonsai:
- 2 compliance proofs (consumed persistent → created persistent)
- 4 logic proofs (2 consumed + 2 created resources)
- 1 delta proof (net balance conservation)
- 1 aggregation proof (combining everything)
- Submits the swap transaction to the Protocol Adapter on Sepolia
- The RPC indexer scans historical blocks to build the commitment Merkle tree
Expected output:
=== AnomaPay Swap Test ===
Maker: 5000000 wei of token 0x94a9D9AC...
Taker: 5000000000000000000 wei of token 0xFF34B3d4...
Maker resource reconstructed (commitment matches)
Taker resource reconstructed (commitment matches)
Action tree root: 0xeedabe4d...
Both parties signed the action tree root.
Generating ZK proofs via Bonsai (this may take several minutes)...
=== SWAP SUCCESS ===
Sepolia TX hash: 0xb21fbcc1...
Consumed (maker): 0x94a9D9AC... 5000000 wei
Consumed (taker): 0xFF34B3d4... 5000000000000000000 wei
Created for taker: 0x94a9D9AC... 5000000 wei
Created for maker: 0xFF34B3d4... 5000000000000000000 wei
After the swap completes, you can verify it on Etherscan:
https://sepolia.etherscan.io/tx/<tx-hash>
| Variable | Required | Description |
|---|---|---|
WALLET_SEED |
Yes* | BIP-39 mnemonic (alternative: FEE_PAYMENT_WALLET_PRIVATE_KEY) |
RPC_URL |
Yes | Ethereum Sepolia RPC endpoint |
ALCHEMY_API_KEY |
Yes | Alchemy API key (used by ForwarderConfig internally) |
BONSAI_API_URL |
Yes | RISC Zero Bonsai prover URL (https://bento.heliax.fyi) |
BONSAI_API_KEY |
Yes | RISC Zero Bonsai API key |
GALILEO_INDEXER_ADDRESS |
wrap only | Galileo indexer URL (e.g. http://localhost:4000). Required by wrap_erc20; the swap_test binary uses its own RPC-based indexer instead. |
ERC20_TOKEN |
wrap only | Token address to wrap (default: Sepolia WETH) |
WRAP_AMOUNT_WEI |
wrap only | Amount to wrap in smallest unit (default: 1000000000000000) |
WRAP_OUTPUT |
wrap only | Output JSON path (default: wrap_output.json) |
MAKER_WRAP_OUTPUT |
swap only | Path to maker's wrap output JSON (default: wrap_maker.json) |
TAKER_WRAP_OUTPUT |
swap only | Path to taker's wrap output JSON (default: wrap_taker.json) |
INDEXER_RPC_URL |
No | RPC for log queries during swap (default: https://ethereum-sepolia-rpc.publicnode.com) |
PA_START_BLOCK |
No | Block to start scanning PA events (default: 10130000) |
| Token | Address | Decimals |
|---|---|---|
| USDC | 0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8 |
6 |
| DAI | 0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357 |
18 |
| Contract | Address |
|---|---|
| ERC20 Forwarder V1 | 0x0A62bE41E66841f693f922991C4e40C89cb0CFDF |
| Protocol Adapter | 0xf152BBA809d6cba122579cee997A54B8F3FBa417 |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| Aave V3 Faucet | 0xC959483DBa39aa9E78757139af0e9a2EDEb3f42D |
"server error Forbidden" during proof generation
- Verify your
BONSAI_API_KEYis correct. Watch out forO(letter) vs0(zero) andl(letter) vs1(one) in base64-encoded keys. - The Bonsai API URL should be
https://bento.heliax.fyi(not any.internalURLs).
"HTTP error from reqwest" during proof generation
- The Bonsai API URL may be unreachable. Verify network connectivity to
https://bento.heliax.fyi.
"Insufficient token balance"
- Run
fund_walletfirst to mint testnet tokens via the Aave Faucet. - Ensure you're using the Aave token addresses, not Circle's native Sepolia USDC.
"Commitment mismatch" during swap
- The
wrap_maker.jsonorwrap_taker.jsonfile may be stale or corrupted. Re-run the wrap step.
Swap takes a long time
- The RPC indexer scans all historical blocks from
PA_START_BLOCKto find existing commitments. This can take a minute on the first run. You can setPA_START_BLOCKto a more recent block to speed things up.
"package swap_app not found"
- Make sure you're on the
jonaprieto/swap-appbranch. Theswap_appcrate does not exist onmain.