@@ -13,22 +13,33 @@ import type {
1313} from '@aztec/ethereum/deploy-aztec-l1-contracts' ;
1414import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract' ;
1515import { pickL1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses' ;
16+ import type { ExtendedViemWalletClient } from '@aztec/ethereum/types' ;
1617import { EpochNumber } from '@aztec/foundation/branded-types' ;
1718import { retryUntil } from '@aztec/foundation/retry' ;
1819import { sleep } from '@aztec/foundation/sleep' ;
19- import { TestERC20Abi , TestERC20Bytecode } from '@aztec/l1-artifacts' ;
20+ import { TestERC20Abi , TestERC20Bytecode , TokenPortalAbi , TokenPortalBytecode } from '@aztec/l1-artifacts' ;
2021import { TokenContract } from '@aztec/noir-contracts.js/Token' ;
2122import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge' ;
2223import type { PXEConfig } from '@aztec/pxe/server' ;
2324import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers' ;
2425import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client' ;
2526
27+ import { mnemonicToAccount } from 'viem/accounts' ;
28+
2629import { MNEMONIC } from '../../fixtures/fixtures.js' ;
2730import { type SetupOptions , ensureAuthRegistryPublished , setup } from '../../fixtures/setup.js' ;
2831import { CrossChainTestHarness } from '../../shared/cross_chain_test_harness.js' ;
2932import type { TestWallet } from '../../test-wallet/test_wallet.js' ;
3033import { SingleNodeTestContext , type SingleNodeTestOpts } from '../single_node_test_context.js' ;
3134
35+ /** Optional configuration for {@link CrossChainMessagingTest}. */
36+ export type CrossChainMessagingTestOpts = {
37+ /** Mnemonic account index for the harness L1 client (token portal, inbox, minting). Defaults to the first account. */
38+ l1HarnessAccountIndex ?: number ;
39+ /** Whether to deploy the L1/L2 token bridge. Set to false for suites that only pass arbitrary messages. Defaults to true. */
40+ deployTokenBridge ?: boolean ;
41+ } ;
42+
3243/**
3344 * The cross-chain-messaging harness over the single-node topology: extends {@link SingleNodeTestContext}
3445 * so it reuses the base node tracking / chain monitor / teardown machinery, but builds its environment
@@ -42,6 +53,9 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
4253 private deployL1ContractsArgs : Partial < DeployAztecL1ContractsArgs > ;
4354 private pxeOpts : Partial < PXEConfig > ;
4455 private l1HarnessAccountIndex ?: number ;
56+ private deployTokenBridge : boolean ;
57+ /** L1 token portal + ERC20 deployed before the node started (under automine) by the setup hook. */
58+ private preDeployedCrossChainL1 ?: { underlyingERC20Address : EthAddress ; tokenPortalAddress : EthAddress } ;
4559 private testName : string ;
4660 aztecNode ! : AztecNode ;
4761 aztecNodeConfig ! : AztecNodeConfig ;
@@ -53,6 +67,8 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
5367 user2Address ! : AztecAddress ;
5468 crossChainTestHarness ! : CrossChainTestHarness ;
5569 ethAccount ! : EthAddress ;
70+ /** L1 client used for cross-chain L1 interactions (token portal, inbox), tied to `l1HarnessAccountIndex`. */
71+ harnessL1Client ! : ExtendedViemWalletClient ;
5672 l2Token ! : TokenContract ;
5773 l2Bridge ! : TokenBridgeContract ;
5874
@@ -77,7 +93,7 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
7793 opts : SetupOptions = { } ,
7894 deployL1ContractsArgs : Partial < DeployAztecL1ContractsArgs > = { } ,
7995 pxeOpts : Partial < PXEConfig > = { } ,
80- l1HarnessAccountIndex ?: number ,
96+ crossChainOpts : CrossChainMessagingTestOpts = { } ,
8197 ) {
8298 super ( ) ;
8399 this . testName = testName ;
@@ -88,7 +104,8 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
88104 ...deployL1ContractsArgs ,
89105 } ;
90106 this . pxeOpts = pxeOpts ;
91- this . l1HarnessAccountIndex = l1HarnessAccountIndex ;
107+ this . l1HarnessAccountIndex = crossChainOpts . l1HarnessAccountIndex ;
108+ this . deployTokenBridge = crossChainOpts . deployTokenBridge ?? true ;
92109 this . requireEpochProven = opts . startProverNode ?? false ;
93110 }
94111
@@ -112,6 +129,35 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
112129 ...opts . proverNodeConfig ,
113130 txGatheringTimeoutMs : opts . proverNodeConfig ?. txGatheringTimeoutMs ?? 10 * 60 * 1000 ,
114131 } ,
132+ // Deploy the cross-chain token portal + ERC20 before the node starts, while anvil is still
133+ // automining, instead of paying the L1 block interval for them once the sequencer is live.
134+ deployExtraL1Contracts : this . deployTokenBridge
135+ ? async ( { l1Client, logger } ) => {
136+ // The harness mints the underlying ERC20 from `l1HarnessAccountIndex` and `TestERC20.mint`
137+ // is onlyMinter, so its owner/initial-minter must be that account even though setup's
138+ // publisher client sends the deploy tx.
139+ const harnessAddress = mnemonicToAccount ( MNEMONIC , {
140+ addressIndex : this . l1HarnessAccountIndex ?? 0 ,
141+ } ) . address ;
142+ const { address : underlyingERC20Address } = await deployL1Contract (
143+ l1Client ,
144+ TestERC20Abi ,
145+ TestERC20Bytecode ,
146+ [ 'Underlying' , 'UND' , harnessAddress ] ,
147+ ) ;
148+ // The TokenPortal's initialize is permissionless, so it can be deployed by the publisher.
149+ const { address : tokenPortalAddress } = await deployL1Contract (
150+ l1Client ,
151+ TokenPortalAbi ,
152+ TokenPortalBytecode ,
153+ ) ;
154+ this . preDeployedCrossChainL1 = { underlyingERC20Address, tokenPortalAddress } ;
155+ logger . verbose (
156+ `Pre-deployed cross-chain L1 ERC20 ${ underlyingERC20Address } and portal ${ tokenPortalAddress } ` ,
157+ ) ;
158+ return this . preDeployedCrossChainL1 ;
159+ }
160+ : undefined ,
115161 } ,
116162 { ...this . pxeOpts , ...pxeOpts } ,
117163 ) ;
@@ -193,12 +239,29 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
193239 undefined ,
194240 this . l1HarnessAccountIndex ,
195241 ) ;
242+ this . harnessL1Client = harnessL1Client ;
243+ this . ethAccount = EthAddress . fromString ( ( await harnessL1Client . getAddresses ( ) ) [ 0 ] ) ;
244+
245+ // L1 contract handles every cross-chain test needs, independent of the token bridge.
246+ const l1Client = createExtendedL1Client ( this . aztecNodeConfig . l1RpcUrls , MNEMONIC ) ;
247+ this . l1Client = l1Client ;
248+ const l1Contracts = pickL1ContractAddresses ( this . aztecNodeConfig ) ;
249+ this . rollup = new RollupContract ( l1Client , l1Contracts . rollupAddress . toString ( ) ) ;
250+ this . inbox = new InboxContract ( l1Client , l1Contracts . inboxAddress . toString ( ) ) ;
251+ this . outbox = new OutboxContract ( l1Client , l1Contracts . outboxAddress . toString ( ) ) ;
196252
197- const underlyingERC20Address = await deployL1Contract ( harnessL1Client , TestERC20Abi , TestERC20Bytecode , [
198- 'Underlying' ,
199- 'UND' ,
200- harnessL1Client . account . address ,
201- ] ) . then ( ( { address } ) => address ) ;
253+ // Tests that only pass arbitrary L1<->L2 messages (not token bridging) never touch the L2 token,
254+ // bridge, or portal. Skip the token+portal+bridge deploy (an L1 ERC20, an L1 portal, and two L2
255+ // contract deploys plus their init txs) entirely for them.
256+ if ( ! this . deployTokenBridge ) {
257+ this . logger . info ( 'Skipping token bridge deploy; test only needs L1 handles and ethAccount' ) ;
258+ return ;
259+ }
260+
261+ // The ERC20 and token portal were deployed before the node started (under automine) by the
262+ // `deployExtraL1Contracts` setup hook above; the harness reuses them and only deploys the L2 token,
263+ // L2 bridge, and the portal init that need the running node.
264+ const { underlyingERC20Address, tokenPortalAddress : predeployedTokenPortalAddress } = this . preDeployedCrossChainL1 ! ;
202265
203266 this . logger . verbose ( `Setting up cross chain harness...` ) ;
204267 this . crossChainTestHarness = await CrossChainTestHarness . new (
@@ -208,6 +271,7 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
208271 this . ownerAddress ,
209272 this . logger ,
210273 underlyingERC20Address ,
274+ predeployedTokenPortalAddress ,
211275 ) ;
212276
213277 this . logger . verbose ( `L2 token deployed to: ${ this . crossChainTestHarness . l2Token . address } ` ) ;
@@ -221,14 +285,6 @@ export class CrossChainMessagingTest extends SingleNodeTestContext {
221285 this . ethAccount = EthAddress . fromString ( crossChainContext . ethAccount . toString ( ) ) ;
222286 const tokenPortalAddress = EthAddress . fromString ( crossChainContext . tokenPortal . toString ( ) ) ;
223287
224- const l1Client = createExtendedL1Client ( this . aztecNodeConfig . l1RpcUrls , MNEMONIC ) ;
225- this . l1Client = l1Client ;
226-
227- const l1Contracts = pickL1ContractAddresses ( this . aztecNodeConfig ) ;
228- this . rollup = new RollupContract ( l1Client , l1Contracts . rollupAddress . toString ( ) ) ;
229- this . inbox = new InboxContract ( l1Client , l1Contracts . inboxAddress . toString ( ) ) ;
230- this . outbox = new OutboxContract ( l1Client , l1Contracts . outboxAddress . toString ( ) ) ;
231-
232288 this . crossChainTestHarness = new CrossChainTestHarness (
233289 this . aztecNode ,
234290 this . logger ,
0 commit comments