Skip to content

Commit

Permalink
refactor: portal managers in cli
Browse files Browse the repository at this point in the history
  • Loading branch information
alexghr authored and sklppy88 committed Aug 16, 2024
1 parent 91efec3 commit bfe896e
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ contract TokenBridge {
}
// docs:end:claim_private

#[aztec(private)]
fn claim_private_for_caller(
amount: Field,
secret_for_L1_to_L2_message_consumption: Field // secret used to consume the L1 to L2 message
) {
// Consume L1 to L2 message and emit nullifier
let content_hash = get_mint_private_content_hash(0, amount);
context.consume_l1_to_l2_message(
content_hash,
secret_for_L1_to_L2_message_consumption,
storage.portal_address.read_private()
);

Token::at(storage.token.read_private()).mint_private_for(context.msg_sender(), amount).call(&mut context);
}

// docs:start:exit_to_l1_private
// Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately
// Requires `msg.sender` (caller of the method) to give approval to the bridge to burn tokens on their behalf using witness signatures
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ contract Token {
}
// docs:end:mint_private

#[aztec(private)]
fn mint_private_for(to: AztecAddress, amount: Field) {
let minter = context.msg_sender();
storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, minter, to));
Token::at(context.this_address()).assert_minter_and_mint(minter, amount).enqueue(&mut context);
}

// TODO: Nuke this - test functions do not belong to token contract!
#[aztec(private)]
fn privately_mint_private_note(amount: Field) {
Expand Down
19 changes: 13 additions & 6 deletions yarn-project/cli/src/cmds/l1/bridge_erc20.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -14,6 +14,7 @@ export async function bridgeERC20(
mnemonic: string,
tokenAddress: EthAddress,
portalAddress: EthAddress,
privateTransfer: boolean,
mint: boolean,
json: boolean,
log: LogFn,
Expand All @@ -24,14 +25,20 @@ 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 portal = await ERC20PortalManager.create(tokenAddress, portalAddress, publicClient, walletClient, debugLogger);
const manager = new L1PortalManager(tokenAddress, portalAddress, 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 {
Expand All @@ -40,7 +47,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`);
}
}
218 changes: 123 additions & 95 deletions yarn-project/cli/src/utils/portal_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,106 +14,107 @@ 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<HttpTransport, Chain>,
public walletClient: WalletClient<HttpTransport, Chain, Account>,
/** 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 enum TransferType {
PRIVATE,
PUBLIC,
}

export interface L2Claim {
claimSecret: Fr;
claimAmount: Fr;
}

function stringifyEthAddress(address: EthAddress | Hex, name?: string) {
return name ? `${name} (${address.toString()})` : address.toString();
}

getERC20Contract(): GetContractReturnType<typeof PortalERC20Abi, WalletClient<HttpTransport, Chain, Account>> {
return getContract({
address: this.underlyingERC20Address.toString(),
function generateClaimSecret(): [Fr, Fr] {
const secret = Fr.random();
const secretHash = computeSecretHash(secret);
return [secret, secretHash];
}

class L1TokenManager {
private contract: GetContractReturnType<typeof PortalERC20Abi, WalletClient<HttpTransport, Chain, Account>>;

public constructor(
public readonly address: EthAddress,
private publicClient: PublicClient<HttpTransport, Chain>,
private walletClient: WalletClient<HttpTransport, Chain, Account>,
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<Hex>;

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(`Minting ${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<Hex> {
const portal = getContract({
address: this.tokenPortalAddress.toString(),
export class FeeJuicePortalManager {
tokenManager: L1TokenManager;
contract: GetContractReturnType<typeof FeeJuicePortalAbi, WalletClient<HttpTransport, Chain, Account>>;

constructor(
portalAddress: EthAddress,
tokenAddress: EthAddress,
private publicClient: PublicClient<HttpTransport, Chain>,
private walletClient: WalletClient<HttpTransport, Chain, Account>,
/** 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<L2Claim> {
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<HttpTransport, Chain>,
walletClient: WalletClient<HttpTransport, Chain, Account>,
logger: DebugLogger,
): Promise<PortalManager> {
): Promise<FeeJuicePortalManager> {
const {
l1ContractAddresses: { feeJuiceAddress, feeJuicePortalAddress },
} = await pxe.getNodeInfo();
Expand All @@ -122,39 +123,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, feeJuicePortalAddress, publicClient, walletClient, logger);
}
}

export class ERC20PortalManager extends PortalManager {
async bridgeTokens(to: AztecAddress, amount: bigint, secretHash: Fr): Promise<Hex> {
const portal = getContract({
address: this.tokenPortalAddress.toString(),
export class L1PortalManager {
contract: GetContractReturnType<typeof TokenPortalAbi, WalletClient<HttpTransport, Chain, Account>>;
private tokenManager: L1TokenManager;

constructor(
portalAddress: EthAddress,
tokenAddress: EthAddress,
private publicClient: PublicClient<HttpTransport, Chain>,
private walletClient: WalletClient<HttpTransport, Chain, Account>,
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<L2Claim> {
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<L2Claim> {
return this.bridgeTokens(to, amount, mint, /* privateTransfer */ true);
}

public static create(
tokenAddress: EthAddress,
portalAddress: EthAddress,
publicClient: PublicClient<HttpTransport, Chain>,
walletClient: WalletClient<HttpTransport, Chain, Account>,
logger: DebugLogger,
): Promise<ERC20PortalManager> {
return Promise.resolve(new ERC20PortalManager(tokenAddress, portalAddress, publicClient, walletClient, logger));
private async bridgeTokens(
to: AztecAddress,
amount: bigint,
mint: boolean,
privateTransfer: boolean,
): Promise<L2Claim> {
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,
};
}
}

0 comments on commit bfe896e

Please sign in to comment.