Created
November 5, 2025 02:48
-
-
Save critesjosh/8bc644126590e1379a4d442507702efd to your computer and use it in GitHub Desktop.
it goes with this tutorial https://docs.aztec.network/devnet/developers/docs/tutorials/js_tutorials/token_bridge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // docs:start:setup | |
| import { privateKeyToAccount } from 'viem/accounts'; | |
| import { mainnet, sepolia } from 'viem/chains' | |
| import { createPublicClient, createWalletClient, http, pad, getAbiItem, toEventHash } from 'viem'; | |
| import { foundry } from 'viem/chains'; | |
| import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; | |
| import { Fr } from '@aztec/aztec.js/fields'; | |
| import { createAztecNodeClient } from '@aztec/aztec.js/node'; | |
| import { computeSecretHash } from '@aztec/stdlib/hash'; | |
| import { computeL2ToL1MembershipWitness } from '@aztec/stdlib/messaging'; | |
| import { sha256ToField } from '@aztec/foundation/crypto'; | |
| import { computeL2ToL1MessageHash } from '@aztec/stdlib/hash'; | |
| import { TestWallet } from '@aztec/test-wallet/server'; | |
| import { getInitialTestAccountsData } from '@aztec/accounts/testing'; | |
| import SimpleNFT from '../artifacts/contracts/SimpleNFT.sol/SimpleNFT.json'; | |
| import NFTPortal from '../artifacts/contracts/NFTPortal.sol/NFTPortal.json'; | |
| import { NFTPunkContract } from '../contracts/aztec/artifacts/NFTPunk.js'; | |
| import { NFTBridgeContract } from '../contracts/aztec/artifacts/NFTBridge.js'; | |
| import { ContractInstanceWithAddress, getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract'; | |
| import { SponsoredFPCContract } from "@aztec/noir-contracts.js/SponsoredFPC"; | |
| import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; | |
| // Setup L1 clients using anvil's 1st account which should have a ton of ETH already | |
| const l1Account = privateKeyToAccount("0xxx"); | |
| console.log(l1Account.address) | |
| const publicClient = createPublicClient({ | |
| chain: sepolia, | |
| transport: http('https://ethereum-sepolia-rpc.publicnode.com'), | |
| }); | |
| const ethWallet = createWalletClient({ | |
| account: l1Account, | |
| chain: sepolia, | |
| transport: http('https://ethereum-sepolia-rpc.publicnode.com'), | |
| }); | |
| // Setup L2 using Aztec's sandbox and one of its initial accounts | |
| console.log('🔮 Setting up L2...\n'); | |
| const node = createAztecNodeClient("https://devnet.aztec-labs.com/"); | |
| const aztecWallet = await TestWallet.create(node); | |
| const SPONSORED_FPC_SALT = new Fr(0); | |
| export async function getSponsoredFPCInstance(): Promise<ContractInstanceWithAddress> { | |
| return await getContractInstanceFromInstantiationParams(SponsoredFPCContract.artifact, { | |
| salt: SPONSORED_FPC_SALT, | |
| }); | |
| } | |
| const sponsoredFPC = await getSponsoredFPCInstance(); | |
| await aztecWallet.registerContract({ instance: sponsoredFPC, artifact: SponsoredFPCContract.artifact }); | |
| const sponsoredPaymentMethod = new SponsoredFeePaymentMethod(sponsoredFPC.address); | |
| // const [accData] = await getInitialTestAccountsData(); | |
| let secretKey = Fr.random(); | |
| let salt = Fr.random(); | |
| const account = await aztecWallet.createSchnorrAccount(secretKey, salt); | |
| let x = (await account.getDeployMethod()).send({ from: AztecAddress.ZERO, fee: { paymentMethod: sponsoredPaymentMethod } }).wait({ timeout: 120000 }) | |
| console.log(`✅ Account: ${account.address.toString()}\n`); | |
| // Get node info | |
| const nodeInfo = await node.getNodeInfo(); | |
| const registryAddress = nodeInfo.l1ContractAddresses.registryAddress.toString(); | |
| const inboxAddress = nodeInfo.l1ContractAddresses.inboxAddress.toString(); | |
| // docs:end:setup | |
| // docs:start:deploy_l1_contracts | |
| console.log('📦 Deploying L1 contracts...\n'); | |
| const nftDeploymentHash = await ethWallet.deployContract({ | |
| abi: SimpleNFT.abi, | |
| bytecode: SimpleNFT.bytecode as `0x${string}`, | |
| }); | |
| const nftReceipt = await publicClient.waitForTransactionReceipt({ hash: nftDeploymentHash }); | |
| const nftAddress = nftReceipt.contractAddress!; | |
| const portalDeploymentHash = await ethWallet.deployContract({ | |
| abi: NFTPortal.abi, | |
| bytecode: NFTPortal.bytecode as `0x${string}`, | |
| }); | |
| const portalReceipt = await publicClient.waitForTransactionReceipt({ hash: portalDeploymentHash }); | |
| const portalAddress = portalReceipt.contractAddress!; | |
| console.log(`✅ SimpleNFT: ${nftAddress}`); | |
| console.log(`✅ NFTPortal: ${portalAddress}\n`); | |
| // docs:end:deploy_l1_contracts | |
| // docs:start:deploy_l2_contracts | |
| console.log('📦 Deploying L2 contracts...\n'); | |
| const l2Nft = await NFTPunkContract.deploy(aztecWallet, account.address) | |
| .send({ from: account.address, fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .deployed(); | |
| const l2Bridge = await NFTBridgeContract.deploy(aztecWallet, l2Nft.address) | |
| .send({ from: account.address, fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .deployed(); | |
| console.log(`✅ L2 NFT: ${l2Nft.address.toString()}`); | |
| console.log(`✅ L2 Bridge: ${l2Bridge.address.toString()}\n`); | |
| // docs:end:deploy_l2_contracts | |
| // docs:start:initialize_portal | |
| console.log('🔧 Initializing portal...'); | |
| const hash = await ethWallet.writeContract({ | |
| address: portalAddress as `0x${string}`, | |
| abi: NFTPortal.abi, | |
| functionName: 'initialize', | |
| args: [ | |
| registryAddress as `0x${string}`, | |
| nftAddress as `0x${string}`, | |
| l2Bridge.address.toString() as `0x${string}` | |
| ], | |
| }); | |
| await publicClient.waitForTransactionReceipt({ hash }); | |
| console.log('✅ Portal initialized\n'); | |
| // docs:end:initialize_portal | |
| // docs:start:initialize_l2_bridge | |
| console.log('🔧 Setting up L2 bridge...'); | |
| await l2Bridge.methods.set_portal(EthAddress.fromString(portalAddress)) | |
| .send({ from: account.address, fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .wait({ timeout: 120000 }); | |
| await l2Nft.methods.set_minter(l2Bridge.address) | |
| .send({ from: account.address, fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .wait({ timeout: 120 }); | |
| console.log('✅ Bridge configured\n'); | |
| // docs:end:initialize_l2_bridge | |
| // docs:start:mint_nft_l1 | |
| console.log('🎨 Minting NFT on L1...'); | |
| const mintHash = await ethWallet.writeContract({ | |
| address: nftAddress as `0x${string}`, | |
| abi: SimpleNFT.abi, | |
| functionName: 'mint', | |
| args: [l1Account.address], | |
| }); | |
| await publicClient.waitForTransactionReceipt({ hash: mintHash }); | |
| // no need to parse logs, this will be tokenId 0 since it's a fresh contract | |
| const tokenId = 0n; | |
| console.log(`✅ Minted tokenId: ${tokenId}\n`); | |
| // docs:end:mint_nft_l1 | |
| // docs:start:deposit_to_aztec | |
| console.log('🌉 Depositing NFT to Aztec...'); | |
| const secret = Fr.random(); | |
| const secretHash = await computeSecretHash(secret); | |
| const approveHash = await ethWallet.writeContract({ | |
| address: nftAddress as `0x${string}`, | |
| abi: SimpleNFT.abi, | |
| functionName: 'approve', | |
| args: [portalAddress as `0x${string}`, tokenId], | |
| }); | |
| await publicClient.waitForTransactionReceipt({ hash: approveHash }); | |
| const depositHash = await ethWallet.writeContract({ | |
| address: portalAddress as `0x${string}`, | |
| abi: NFTPortal.abi, | |
| functionName: 'depositToAztec', | |
| args: [tokenId, pad(secretHash.toString() as `0x${string}`, { dir: 'left', size: 32 })], | |
| }); | |
| const depositReceipt = await publicClient.waitForTransactionReceipt({ hash: depositHash }); | |
| // docs:end:deposit_to_aztec | |
| // docs:start:get_message_leaf_index | |
| const INBOX_ABI = [{ | |
| type: 'event', | |
| name: 'MessageSent', | |
| inputs: [ | |
| { name: 'l2BlockNumber', type: 'uint256', indexed: true }, | |
| { name: 'index', type: 'uint256', indexed: false }, | |
| { name: 'hash', type: 'bytes32', indexed: true }, | |
| { name: 'rollingHash', type: 'bytes16', indexed: false } | |
| ] | |
| }] as const; | |
| const messageSentTopic = toEventHash(INBOX_ABI[0]); | |
| const messageSentLog = depositReceipt.logs!.find( | |
| (log: any) => log.address.toLowerCase() === inboxAddress.toLowerCase() && | |
| log.topics[0] === messageSentTopic | |
| ); | |
| const indexHex = messageSentLog!.data!.slice(0, 66); | |
| const messageLeafIndex = new Fr(BigInt(indexHex)); | |
| // docs:end:get_message_leaf_index | |
| // docs:start:mine_blocks | |
| async function mine2Blocks(aztecWallet: TestWallet, accountAddress: any) { | |
| await NFTPunkContract.deploy(aztecWallet, accountAddress) | |
| .send({ from: accountAddress, contractAddressSalt: Fr.random(), fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .deployed(); | |
| await NFTPunkContract.deploy(aztecWallet, accountAddress) | |
| .send({ from: accountAddress, contractAddressSalt: Fr.random(), fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .deployed(); | |
| } | |
| // docs:end:mine_blocks | |
| // docs:start:claim_on_l2 | |
| // Mine blocks | |
| await mine2Blocks(aztecWallet, account.address); | |
| // Check notes before claiming (should be 0) | |
| console.log('📝 Checking notes before claim...'); | |
| const notesBefore = await l2Nft.methods.notes_of(account.address).simulate({ from: account.address }); | |
| console.log(` Notes count: ${notesBefore}`); | |
| console.log('🎯 Claiming NFT on L2...'); | |
| await l2Bridge.methods.claim( | |
| account.address, | |
| new Fr(Number(tokenId)), | |
| secret, | |
| messageLeafIndex | |
| ) | |
| .send({ from: account.address, fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .wait(); | |
| console.log('✅ NFT claimed on L2\n'); | |
| // Check notes after claiming (should be 1) | |
| console.log('📝 Checking notes after claim...'); | |
| const notesAfterClaim = await l2Nft.methods.notes_of(account.address).simulate({ from: account.address }); | |
| console.log(` Notes count: ${notesAfterClaim}\n`); | |
| // docs:end:claim_on_l2 | |
| // docs:start:exit_from_l2 | |
| // L2 → L1 flow | |
| console.log('🚪 Exiting NFT from L2...'); | |
| // Mine blocks | |
| await mine2Blocks(aztecWallet, account.address); | |
| const recipientEthAddress = EthAddress.fromString(l1Account.address); | |
| const exitReceipt = await l2Bridge.methods.exit( | |
| new Fr(Number(tokenId)), | |
| recipientEthAddress | |
| ) | |
| .send({ from: account.address, fee: { paymentMethod: sponsoredPaymentMethod } }) | |
| .wait({ timeout: 120000 }); | |
| console.log(`✅ Exit message sent (block: ${exitReceipt.blockNumber})\n`); | |
| // Check notes after burning (should be 0 again) | |
| console.log('📝 Checking notes after burn...'); | |
| const notesAfterBurn = await l2Nft.methods.notes_of(account.address).simulate({ from: account.address }); | |
| console.log(` Notes count: ${notesAfterBurn}\n`); | |
| // docs:end:exit_from_l2 | |
| // docs:start:get_withdrawal_witness | |
| // Compute the message hash directly from known parameters | |
| // This matches what the portal contract expects: Hash.sha256ToField(abi.encodePacked(tokenId, recipient)) | |
| const tokenIdBuffer = new Fr(Number(tokenId)).toBuffer(); | |
| const recipientBuffer = Buffer.from(recipientEthAddress.toString().slice(2), 'hex'); | |
| const content = sha256ToField([Buffer.concat([tokenIdBuffer, recipientBuffer])]); | |
| // Get rollup version from the portal contract (it stores it during initialize) | |
| const version = await publicClient.readContract({ | |
| address: portalAddress as `0x${string}`, | |
| abi: NFTPortal.abi, | |
| functionName: 'rollupVersion' | |
| }) as number; | |
| // Compute the L2→L1 message hash | |
| const msgLeaf = computeL2ToL1MessageHash({ | |
| l2Sender: l2Bridge.address, | |
| l1Recipient: EthAddress.fromString(portalAddress), | |
| content, | |
| rollupVersion: new Fr(version), | |
| chainId: new Fr(sepolia.id), | |
| }); | |
| // Wait for the block to be proven before withdrawing | |
| console.log('⏳ Waiting for block to be proven...'); | |
| console.log(` Exit block number: ${exitReceipt.blockNumber}`); | |
| let provenBlockNumber = await node.getProvenBlockNumber(); | |
| console.log(` Current proven block: ${provenBlockNumber}`); | |
| while (provenBlockNumber < exitReceipt.blockNumber!) { | |
| console.log(` Waiting... (proven: ${provenBlockNumber}, needed: ${exitReceipt.blockNumber})`); | |
| await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds | |
| provenBlockNumber = await node.getProvenBlockNumber(); | |
| } | |
| console.log('✅ Block proven!\n'); | |
| // Compute the membership witness using the message hash | |
| const witness = await computeL2ToL1MembershipWitness(node, exitReceipt.blockNumber!, msgLeaf); | |
| console.log('🔍 Witness computed successfully'); | |
| console.log(' Block number:', exitReceipt.blockNumber); | |
| console.log(' Leaf index:', witness?.leafIndex); | |
| console.log(' Sibling path length:', witness?.siblingPath.toBufferArray().length); | |
| const siblingPathHex = witness!.siblingPath.toBufferArray().map((buf: Buffer) => | |
| `0x${buf.toString('hex')}` as `0x${string}` | |
| ); | |
| // docs:end:get_withdrawal_witness | |
| // docs:start:withdraw_on_l1 | |
| console.log('💰 Withdrawing NFT on L1...'); | |
| console.log(' Portal address:', portalAddress); | |
| console.log(' Token ID:', tokenId); | |
| console.log(' Block number:', exitReceipt.blockNumber); | |
| console.log(' Leaf index:', witness!.leafIndex); | |
| console.log(' Sibling path count:', siblingPathHex.length); | |
| try { | |
| const withdrawHash = await ethWallet.writeContract({ | |
| address: portalAddress as `0x${string}`, | |
| abi: NFTPortal.abi, | |
| functionName: 'withdraw', | |
| args: [ | |
| tokenId, | |
| BigInt(exitReceipt.blockNumber!), | |
| BigInt(witness!.leafIndex), | |
| siblingPathHex, | |
| ], | |
| }); | |
| await publicClient.waitForTransactionReceipt({ hash: withdrawHash }); | |
| console.log('✅ NFT withdrawn to L1\n'); | |
| } catch (error) { | |
| console.error('❌ Withdraw failed:', error); | |
| throw error; | |
| } | |
| // docs:end:withdraw_on_l1 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment