From 618e2510d760e8749d1993f84f08fd7adb7683b2 Mon Sep 17 00:00:00 2001 From: esau <152162806+sklppy88@users.noreply.github.com> Date: Tue, 20 Aug 2024 22:10:09 +0200 Subject: [PATCH] refactor: portal manager cli (#8047) Please read [contributing guidelines](CONTRIBUTING.md) and remove this line. --- .../cli-wallet/src/cmds/bridge_fee_juice.ts | 16 +- yarn-project/cli-wallet/src/cmds/index.ts | 13 +- .../cli/src/cmds/devnet/bootstrap_network.ts | 9 +- yarn-project/cli/src/cmds/l1/bridge_erc20.ts | 18 +- yarn-project/cli/src/cmds/l1/index.ts | 2 + yarn-project/cli/src/utils/portal_manager.ts | 213 ++++++++++-------- 6 files changed, 157 insertions(+), 114 deletions(-) diff --git a/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts b/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts index ad6e539f6b3..436b4f6a218 100644 --- a/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts +++ b/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts @@ -25,23 +25,23 @@ export async function bridgeL1FeeJuice( const client = await createCompatibleClient(rpcUrl, debugLogger); // Setup portal manager - const portal = await FeeJuicePortalManager.create(client, publicClient, walletClient, debugLogger); - const { secret } = await portal.prepareTokensOnL1(amount, amount, recipient, mint); + const portal = await FeeJuicePortalManager.new(client, publicClient, walletClient, debugLogger); + const { claimAmount, claimSecret } = await portal.bridgeTokensPublic(recipient, amount, mint); if (json) { const out = { - claimAmount: amount, - claimSecret: secret, + claimAmount, + claimSecret, }; log(prettyPrintJSON(out)); } else { if (mint) { - log(`Minted ${amount} fee juice on L1 and pushed to L2 portal`); + log(`Minted ${claimAmount} fee juice on L1 and pushed to L2 portal`); } else { - log(`Bridged ${amount} fee juice to L2 portal`); + log(`Bridged ${claimAmount} fee juice to L2 portal`); } - log(`claimAmount=${amount},claimSecret=${secret}\n`); + log(`claimAmount=${claimAmount},claimSecret=${claimSecret}\n`); log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); } - return secret; + return claimSecret; } diff --git a/yarn-project/cli-wallet/src/cmds/index.ts b/yarn-project/cli-wallet/src/cmds/index.ts index 74d8373b680..fc1ff2c6608 100644 --- a/yarn-project/cli-wallet/src/cmds/index.ts +++ b/yarn-project/cli-wallet/src/cmds/index.ts @@ -354,6 +354,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .requiredOption('-a, --address ', 'The Aztec address of the note owner.', address => aliasedAddressParser('accounts', address, db), ) + .addOption(createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db))) .requiredOption('-h, --hash ', 'The tx hash of the tx containing the note.', txHash => aliasedTxHashParser(txHash, db), ) @@ -372,10 +373,18 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .action(async (noteName, storageFieldName, _options, command) => { const { addNote } = await import('./add_note.js'); const options = command.optsWithGlobals(); - const { contractArtifact: artifactPathPromise, contractAddress, address, rpcUrl, fields, hash } = options; + const { + contractArtifact: artifactPathPromise, + contractAddress, + address, + secretKey, + rpcUrl, + fields, + hash, + } = options; const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, address, db); + const account = await createOrRetrieveAccount(client, address, db, undefined, secretKey); const wallet = await account.getWallet(); await addNote(wallet, address, contractAddress, noteName, storageFieldName, artifactPath, hash, fields, log); diff --git a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts index f76fe467596..8030b0dcfa4 100644 --- a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts +++ b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts @@ -231,7 +231,7 @@ async function fundFPC( const feeJuiceContract = await FeeJuiceContract.at(feeJuice, wallet); - const feeJuicePortal = await FeeJuicePortalManager.create( + const feeJuicePortal = await FeeJuicePortalManager.new( wallet, l1Clients.publicClient, l1Clients.walletClient, @@ -239,7 +239,7 @@ async function fundFPC( ); const amount = 10n ** 21n; - const { secret } = await feeJuicePortal.prepareTokensOnL1(amount, amount, fpcAddress, true); + const { claimAmount, claimSecret } = await feeJuicePortal.bridgeTokensPublic(fpcAddress, amount, true); const counter = await CounterContract.at(counterAddress, wallet); @@ -254,5 +254,8 @@ async function fundFPC( .send() .wait({ proven: true, provenTimeout: 600 }); - await feeJuiceContract.methods.claim(fpcAddress, amount, secret).send().wait({ proven: true, provenTimeout: 600 }); + await feeJuiceContract.methods + .claim(fpcAddress, claimAmount, claimSecret) + .send() + .wait({ proven: true, provenTimeout: 600 }); } diff --git a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts index bd9d7d1d957..d93b705068d 100644 --- a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts +++ b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts @@ -1,9 +1,9 @@ -import { type AztecAddress, type EthAddress } from '@aztec/circuits.js'; +import { type AztecAddress, type EthAddress, type Fr } from '@aztec/circuits.js'; import { createEthereumChain, createL1Clients } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { prettyPrintJSON } from '../../utils/commands.js'; -import { ERC20PortalManager } from '../../utils/portal_manager.js'; +import { L1PortalManager } from '../../utils/portal_manager.js'; export async function bridgeERC20( amount: bigint, @@ -14,6 +14,7 @@ export async function bridgeERC20( mnemonic: string, tokenAddress: EthAddress, portalAddress: EthAddress, + privateTransfer: boolean, mint: boolean, json: boolean, log: LogFn, @@ -24,14 +25,19 @@ export async function bridgeERC20( const { publicClient, walletClient } = createL1Clients(chain.rpcUrl, privateKey ?? mnemonic, chain.chainInfo); // Setup portal manager - const portal = await ERC20PortalManager.create(tokenAddress, portalAddress, publicClient, walletClient, debugLogger); - const { secret } = await portal.prepareTokensOnL1(amount, amount, recipient, mint); + const manager = new L1PortalManager(portalAddress, tokenAddress, publicClient, walletClient, debugLogger); + let claimSecret: Fr; + if (privateTransfer) { + ({ claimSecret } = await manager.bridgeTokensPrivate(recipient, amount, mint)); + } else { + ({ claimSecret } = await manager.bridgeTokensPublic(recipient, amount, mint)); + } if (json) { log( prettyPrintJSON({ claimAmount: amount, - claimSecret: secret, + claimSecret: claimSecret, }), ); } else { @@ -40,7 +46,7 @@ export async function bridgeERC20( } else { log(`Bridged ${amount} tokens to L2 portal`); } - log(`claimAmount=${amount},claimSecret=${secret}\n`); + log(`claimAmount=${amount},claimSecret=${claimSecret}\n`); log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); } } diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts index febe2a446ca..4ebfeb587ba 100644 --- a/yarn-project/cli/src/cmds/l1/index.ts +++ b/yarn-project/cli/src/cmds/l1/index.ts @@ -108,6 +108,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL 'test test test test test test test test test test test junk', ) .option('--mint', 'Mint the tokens on L1', false) + .option('--private', 'If the bridge should use the private flow', false) .addOption(l1ChainIdOption) .requiredOption('-t, --token ', 'The address of the token to bridge', parseEthereumAddress) .requiredOption('-p, --portal ', 'The address of the portal contract', parseEthereumAddress) @@ -124,6 +125,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL options.mnemonic, options.token, options.portal, + options.private, options.mint, options.json, log, diff --git a/yarn-project/cli/src/utils/portal_manager.ts b/yarn-project/cli/src/utils/portal_manager.ts index 19a0829b2f0..4e25af870e2 100644 --- a/yarn-project/cli/src/utils/portal_manager.ts +++ b/yarn-project/cli/src/utils/portal_manager.ts @@ -14,106 +14,102 @@ import { getContract, } from 'viem'; -/** - * A Class for testing cross chain interactions, contains common interactions - * shared between cross chain tests. - */ -abstract class PortalManager { - protected constructor( - /** Underlying token for portal tests. */ - public underlyingERC20Address: EthAddress, - /** Portal address. */ - public tokenPortalAddress: EthAddress, - public publicClient: PublicClient, - public walletClient: WalletClient, - /** Logger. */ - public logger: DebugLogger, - ) {} - - generateClaimSecret(): [Fr, Fr] { - this.logger.debug("Generating a claim secret using pedersen's hash function"); - const secret = Fr.random(); - const secretHash = computeSecretHash(secret); - this.logger.info('Generated claim secret: ' + secretHash.toString()); - return [secret, secretHash]; - } +export interface L2Claim { + claimSecret: Fr; + claimAmount: Fr; +} + +function stringifyEthAddress(address: EthAddress | Hex, name?: string) { + return name ? `${name} (${address.toString()})` : address.toString(); +} + +function generateClaimSecret(): [Fr, Fr] { + const secret = Fr.random(); + const secretHash = computeSecretHash(secret); + return [secret, secretHash]; +} - getERC20Contract(): GetContractReturnType> { - return getContract({ - address: this.underlyingERC20Address.toString(), +class L1TokenManager { + private contract: GetContractReturnType>; + + public constructor( + public readonly address: EthAddress, + private publicClient: PublicClient, + private walletClient: WalletClient, + private logger: DebugLogger, + ) { + this.contract = getContract({ + address: this.address.toString(), abi: PortalERC20Abi, client: this.walletClient, }); } - async mintTokensOnL1(amount: bigint) { - this.logger.info( - `Minting tokens on L1 for ${this.walletClient.account.address} in contract ${this.underlyingERC20Address}`, - ); - await this.publicClient.waitForTransactionReceipt({ - hash: await this.getERC20Contract().write.mint([this.walletClient.account.address, amount]), - }); - } - - async getL1TokenBalance(address: EthAddress) { - return await this.getERC20Contract().read.balanceOf([address.toString()]); + public async getL1TokenBalance(address: Hex) { + return await this.contract.read.balanceOf([address]); } - protected async sendTokensToPortalPublic(bridgeAmount: bigint, l2Address: AztecAddress, secretHash: Fr) { - this.logger.info(`Approving erc20 tokens for the TokenPortal at ${this.tokenPortalAddress.toString()}`); + public async mint(amount: bigint, address: Hex, addressName = '') { + this.logger.info(`Minting ${amount} tokens for ${stringifyEthAddress(address, addressName)}`); await this.publicClient.waitForTransactionReceipt({ - hash: await this.getERC20Contract().write.approve([this.tokenPortalAddress.toString(), bridgeAmount]), + hash: await this.contract.write.mint([address, amount]), }); - - const messageHash = await this.bridgeTokens(l2Address, bridgeAmount, secretHash); - return Fr.fromString(messageHash); } - protected abstract bridgeTokens(to: AztecAddress, amount: bigint, secretHash: Fr): Promise; - - async prepareTokensOnL1(l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress, mint = true) { - const [secret, secretHash] = this.generateClaimSecret(); - - // Mint tokens on L1 - if (mint) { - await this.mintTokensOnL1(l1TokenBalance); - } - - // Deposit tokens to the TokenPortal - const msgHash = await this.sendTokensToPortalPublic(bridgeAmount, owner, secretHash); - - return { secret, msgHash, secretHash }; + public async approve(amount: bigint, address: Hex, addressName = '') { + this.logger.info(`Approving ${amount} tokens for ${stringifyEthAddress(address, addressName)}`); + await this.publicClient.waitForTransactionReceipt({ + hash: await this.contract.write.approve([address, amount]), + }); } } -export class FeeJuicePortalManager extends PortalManager { - async bridgeTokens(to: AztecAddress, amount: bigint, secretHash: Fr): Promise { - const portal = getContract({ - address: this.tokenPortalAddress.toString(), +export class FeeJuicePortalManager { + tokenManager: L1TokenManager; + contract: GetContractReturnType>; + + constructor( + portalAddress: EthAddress, + tokenAddress: EthAddress, + private publicClient: PublicClient, + private walletClient: WalletClient, + /** Logger. */ + private logger: DebugLogger, + ) { + this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger); + this.contract = getContract({ + address: portalAddress.toString(), abi: FeeJuicePortalAbi, client: this.walletClient, }); + } - this.logger.info( - `Simulating token portal deposit configured for token ${await portal.read.l2TokenAddress()} with registry ${await portal.read.registry()} to retrieve message hash`, - ); + public async bridgeTokensPublic(to: AztecAddress, amount: bigint, mint = false): Promise { + const [claimSecret, claimSecretHash] = generateClaimSecret(); + if (mint) { + await this.tokenManager.mint(amount, this.walletClient.account.address); + } - const args = [to.toString(), amount, secretHash.toString()] as const; - const { result: messageHash } = await portal.simulate.depositToAztecPublic(args); - this.logger.info('Sending messages to L1 portal to be consumed publicly'); + await this.tokenManager.approve(amount, this.contract.address, 'FeeJuice Portal'); + this.logger.info('Sending L1 Fee Juice to L2 to be claimed publicly'); + const args = [to.toString(), amount, claimSecretHash.toString()] as const; await this.publicClient.waitForTransactionReceipt({ - hash: await portal.write.depositToAztecPublic(args), + hash: await this.contract.write.depositToAztecPublic(args), }); - return messageHash; + + return { + claimAmount: new Fr(amount), + claimSecret, + }; } - public static async create( + public static async new( pxe: PXE, publicClient: PublicClient, walletClient: WalletClient, logger: DebugLogger, - ): Promise { + ): Promise { const { l1ContractAddresses: { feeJuiceAddress, feeJuicePortalAddress }, } = await pxe.getNodeInfo(); @@ -122,39 +118,66 @@ export class FeeJuicePortalManager extends PortalManager { throw new Error('Portal or token not deployed on L1'); } - return new FeeJuicePortalManager(feeJuiceAddress, feeJuicePortalAddress, publicClient, walletClient, logger); + return new FeeJuicePortalManager(feeJuicePortalAddress, feeJuiceAddress, publicClient, walletClient, logger); } } -export class ERC20PortalManager extends PortalManager { - async bridgeTokens(to: AztecAddress, amount: bigint, secretHash: Fr): Promise { - const portal = getContract({ - address: this.tokenPortalAddress.toString(), +export class L1PortalManager { + contract: GetContractReturnType>; + private tokenManager: L1TokenManager; + + constructor( + portalAddress: EthAddress, + tokenAddress: EthAddress, + private publicClient: PublicClient, + private walletClient: WalletClient, + private logger: DebugLogger, + ) { + this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger); + this.contract = getContract({ + address: portalAddress.toString(), abi: TokenPortalAbi, client: this.walletClient, }); + } - this.logger.info( - `Simulating token portal deposit configured for token ${await portal.read.l2Bridge()} with registry ${await portal.read.registry()} to retrieve message hash`, - ); - - const args = [to.toString(), amount, secretHash.toString()] as const; - const { result: messageHash } = await portal.simulate.depositToAztecPublic(args); - this.logger.info('Sending messages to L1 portal to be consumed publicly'); + public bridgeTokensPublic(to: AztecAddress, amount: bigint, mint = false): Promise { + return this.bridgeTokens(to, amount, mint, /* privateTransfer */ false); + } - await this.publicClient.waitForTransactionReceipt({ - hash: await portal.write.depositToAztecPublic(args), - }); - return messageHash; + public bridgeTokensPrivate(to: AztecAddress, amount: bigint, mint = false): Promise { + return this.bridgeTokens(to, amount, mint, /* privateTransfer */ true); } - public static create( - tokenAddress: EthAddress, - portalAddress: EthAddress, - publicClient: PublicClient, - walletClient: WalletClient, - logger: DebugLogger, - ): Promise { - return Promise.resolve(new ERC20PortalManager(tokenAddress, portalAddress, publicClient, walletClient, logger)); + private async bridgeTokens( + to: AztecAddress, + amount: bigint, + mint: boolean, + privateTransfer: boolean, + ): Promise { + const [claimSecret, claimSecretHash] = generateClaimSecret(); + + if (mint) { + await this.tokenManager.mint(amount, this.walletClient.account.address); + } + + await this.tokenManager.approve(amount, this.contract.address, 'TokenPortal'); + + if (privateTransfer) { + this.logger.info('Sending L1 tokens to L2 to be claimed privately'); + await this.publicClient.waitForTransactionReceipt({ + hash: await this.contract.write.depositToAztecPrivate([Fr.ZERO.toString(), amount, claimSecretHash.toString()]), + }); + } else { + this.logger.info('Sending L1 tokens to L2 to be claimed publicly'); + await this.publicClient.waitForTransactionReceipt({ + hash: await this.contract.write.depositToAztecPublic([to.toString(), amount, claimSecretHash.toString()]), + }); + } + + return { + claimAmount: new Fr(amount), + claimSecret, + }; } }