@aztec-labs/cli 6.0.0-rc.1 → 6.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,31 @@
1
1
  import { RollupAbi, StakingAssetHandlerAbi, TestERC20Abi } from '@aztec-foundation/l1-artifacts';
2
2
 
3
3
  import { createEthereumChain, isAnvilTestChain } from '@aztec-labs/ethereum/chain';
4
- import { createExtendedL1Client, getPublicClient } from '@aztec-labs/ethereum/client';
4
+ import { createExtendedL1Client, getPublicClient, makeL1HttpTransport } from '@aztec-labs/ethereum/client';
5
5
  import { getL1ContractsConfigEnvVars } from '@aztec-labs/ethereum/config';
6
- import { type AttesterExitAuthorization, GSEContract, RollupContract } from '@aztec-labs/ethereum/contracts';
6
+ import {
7
+ type AttesterExitAuthorization,
8
+ GSEContract,
9
+ RegistryContract,
10
+ RollupContract,
11
+ } from '@aztec-labs/ethereum/contracts';
7
12
  import { createL1TxUtils } from '@aztec-labs/ethereum/l1-tx-utils';
8
13
  import { EthCheatCodes } from '@aztec-labs/ethereum/test';
14
+ import { getActiveNetworkName } from '@aztec-labs/foundation/config';
9
15
  import { EthAddress } from '@aztec-labs/foundation/eth-address';
10
16
  import { Signature } from '@aztec-labs/foundation/eth-signature';
11
17
  import type { LogFn, Logger } from '@aztec-labs/foundation/log';
12
18
  import { DateProvider } from '@aztec-labs/foundation/timer';
13
19
  import { ZkPassportProofParams } from '@aztec-labs/stdlib/zkpassport';
14
- import { readFile } from 'node:fs/promises';
15
- import { encodeFunctionData, formatEther, getContract, isHex, maxUint256 } from 'viem';
20
+ import { readFile, writeFile } from 'node:fs/promises';
21
+ import { join } from 'node:path';
22
+ import { createPublicClient, encodeFunctionData, formatEther, getContract, isHex, maxUint256 } from 'viem';
16
23
  import { generatePrivateKey, mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
17
24
 
25
+ import { getNetworkConfig } from '../../config/network_config.js';
26
+ import { atomicUpdateFile } from '../../utils/commands.js';
27
+ import { deriveEthAttester } from '../validator_keys/shared.js';
28
+
18
29
  export interface RollupCommandArgs {
19
30
  rpcUrls: string[];
20
31
  chainId: number;
@@ -24,6 +35,14 @@ export interface RollupCommandArgs {
24
35
  withdrawerAddress?: EthAddress;
25
36
  }
26
37
 
38
+ /** Credentials and derivation indices for a local Ethereum signer. */
39
+ export type SignerAccountArgs = {
40
+ privateKey?: string;
41
+ mnemonic?: string;
42
+ accountIndex?: number;
43
+ addressIndex?: number;
44
+ };
45
+
27
46
  export interface StakingAssetHandlerCommandArgs {
28
47
  rpcUrls: string[];
29
48
  chainId: number;
@@ -256,16 +275,172 @@ export async function removeL1Validator({
256
275
  dualLog(`Transaction hash: ${receipt.transactionHash}`);
257
276
  }
258
277
 
278
+ /** Chain ID and registry published for a network; both are undefined when it publishes none, as for `local`. */
279
+ export type AttesterExitNetwork = {
280
+ name: string;
281
+ l1ChainId?: number;
282
+ registryAddress?: EthAddress;
283
+ };
284
+
285
+ /** Loads the published config of the network selected with --network, which takes precedence over the environment. */
286
+ export async function getAttesterExitNetwork(name: string): Promise<AttesterExitNetwork> {
287
+ const networkName = getActiveNetworkName(name);
288
+ const cacheDir = process.env.DATA_DIRECTORY ? join(process.env.DATA_DIRECTORY, 'cache') : undefined;
289
+ const config = networkName === 'local' ? undefined : await getNetworkConfig(networkName, cacheDir);
290
+ if (networkName !== 'local' && !config) {
291
+ throw new Error(`Network ${networkName} has no published config`);
292
+ }
293
+ return {
294
+ name: networkName,
295
+ l1ChainId: config?.l1ChainId,
296
+ registryAddress: config ? EthAddress.fromString(config.registryAddress) : undefined,
297
+ };
298
+ }
299
+
300
+ /** Inputs that select the chain and rollup an attester exit is signed for; each is undefined when not supplied. */
301
+ export type AttesterExitTargetArgs = {
302
+ chainId?: number;
303
+ /** Whether `chainId` came from the L1_CHAIN_ID environment variable rather than a flag. */
304
+ chainIdFromEnv?: boolean;
305
+ rpcUrls?: string[];
306
+ network?: AttesterExitNetwork;
307
+ rollupAddress?: EthAddress;
308
+ log: LogFn;
309
+ };
310
+
311
+ /**
312
+ * Resolves the chain ID and rollup an attester exit is signed for, without falling back to the Anvil chain ID.
313
+ * Whenever an RPC is given, its chain ID must match. Without one, signing is offline and needs the chain ID from a flag
314
+ * or --network; an L1_CHAIN_ID left in the environment alone is rejected because nothing confirms it.
315
+ */
316
+ export async function resolveAttesterExitTarget({
317
+ chainId,
318
+ chainIdFromEnv = false,
319
+ rpcUrls,
320
+ network,
321
+ rollupAddress,
322
+ log,
323
+ }: AttesterExitTargetArgs): Promise<{ chainId: number; rollupAddress: EthAddress }> {
324
+ if (!rollupAddress && !network) {
325
+ throw new Error('Provide --rollup, or --network to use the canonical rollup from the network registry');
326
+ }
327
+ if (!rollupAddress && !network?.registryAddress) {
328
+ throw new Error(`Network ${network?.name} publishes no registry address; provide --rollup`);
329
+ }
330
+ if (chainId !== undefined) {
331
+ assertValidChainId(chainId);
332
+ if (network?.l1ChainId !== undefined && chainId !== network.l1ChainId) {
333
+ throw new Error(`Chain ID ${chainId} does not match ${network.name}, which uses chain ID ${network.l1ChainId}`);
334
+ }
335
+ }
336
+ const expectedChainId = chainId ?? network?.l1ChainId;
337
+ if (!rpcUrls) {
338
+ if (expectedChainId === undefined) {
339
+ throw new Error('Provide --l1-chain-id, --network, or --l1-rpc-urls to select the chain to sign for');
340
+ }
341
+ if (!rollupAddress) {
342
+ throw new Error('Looking up the rollup in the network registry requires --l1-rpc-urls');
343
+ }
344
+ if (chainIdFromEnv && network?.l1ChainId === undefined) {
345
+ throw new Error(
346
+ `Chain ID ${expectedChainId} comes only from L1_CHAIN_ID; confirm it with --l1-chain-id, --network, or --l1-rpc-urls`,
347
+ );
348
+ }
349
+ return { chainId: expectedChainId, rollupAddress };
350
+ }
351
+ const rpcChainId = await getL1RpcChainId(rpcUrls, expectedChainId);
352
+ if (rollupAddress) {
353
+ return { chainId: rpcChainId, rollupAddress };
354
+ }
355
+ const registryAddress = network!.registryAddress!;
356
+ const client = getPublicClient({ l1RpcUrls: rpcUrls, l1ChainId: rpcChainId });
357
+ const canonicalRollup = await new RegistryContract(client, registryAddress).getCanonicalAddress();
358
+ log(`Using canonical rollup ${canonicalRollup} from the ${network!.name} registry ${registryAddress}`);
359
+ return { chainId: rpcChainId, rollupAddress: canonicalRollup };
360
+ }
361
+
362
+ /** Returns the chain ID the L1 RPC reports, failing if it differs from `expectedChainId`. */
363
+ async function getL1RpcChainId(rpcUrls: string[], expectedChainId?: number): Promise<number> {
364
+ const rpcChainId = await createPublicClient({ transport: makeL1HttpTransport(rpcUrls) }).getChainId();
365
+ if (expectedChainId !== undefined && rpcChainId !== expectedChainId) {
366
+ throw new Error(`The L1 RPC reports chain ID ${rpcChainId}, but chain ID ${expectedChainId} was requested`);
367
+ }
368
+ return rpcChainId;
369
+ }
370
+
371
+ /** Signs an exit authorization locally and writes a JSON array accepted by the batch command. */
372
+ export async function signAttesterExit({
373
+ rpcUrls,
374
+ chainId,
375
+ privateKey,
376
+ mnemonic,
377
+ accountIndex,
378
+ addressIndex,
379
+ rollupAddress,
380
+ attesterAddress,
381
+ deadline,
382
+ output,
383
+ append = false,
384
+ log,
385
+ }: Omit<RollupCommandArgs, 'withdrawerAddress' | 'mnemonic' | 'privateKey'> &
386
+ SignerAccountArgs & {
387
+ attesterAddress: EthAddress;
388
+ deadline: bigint;
389
+ output: string;
390
+ append?: boolean;
391
+ log: LogFn;
392
+ }) {
393
+ assertValidChainId(chainId);
394
+ const account = getSignerAccount({ privateKey, mnemonic, accountIndex, addressIndex });
395
+ if (account.address.toLowerCase() !== attesterAddress.toString().toLowerCase()) {
396
+ throw new Error('The signing account must match the attester address');
397
+ }
398
+ const chain = createEthereumChain(rpcUrls, chainId);
399
+ const client = createExtendedL1Client(rpcUrls, account, chain.chainInfo);
400
+ const rollup = new RollupContract(client, rollupAddress);
401
+ const authorization = await rollup.createAttesterExitAuthorization(attesterAddress, deadline, typedData =>
402
+ account.signTypedData(typedData),
403
+ );
404
+ const existingAuthorizations = append
405
+ ? await readAttesterExitAuthorizationJson(output).catch((error: unknown) => {
406
+ if (!isRecord(error) || error.code !== 'ENOENT') {
407
+ throw error;
408
+ }
409
+ return undefined;
410
+ })
411
+ : undefined;
412
+ const authorizations = existingAuthorizations ?? [];
413
+ authorizations.push({
414
+ attester: authorization.attester.toString(),
415
+ deadline: authorization.deadline.toString(),
416
+ signature: Signature.fromViemSignature(authorization.signature).toString(),
417
+ });
418
+ const json = JSON.stringify(authorizations, null, 2);
419
+ if (existingAuthorizations) {
420
+ await atomicUpdateFile(output, `${json}\n`);
421
+ } else {
422
+ await writeFile(output, `${json}\n`, { flag: 'wx' });
423
+ }
424
+ log(`Wrote attester exit authorization to ${output}`);
425
+ }
426
+
259
427
  /** Reads relayed attester exit authorizations from a JSON array. */
260
428
  export async function readAttesterExitAuthorizations(path: string): Promise<AttesterExitAuthorization[]> {
261
- const parsed: unknown = JSON.parse(await readFile(path, 'utf-8'));
262
- if (!Array.isArray(parsed) || parsed.length === 0) {
429
+ const parsed = await readAttesterExitAuthorizationJson(path);
430
+ if (parsed.length === 0) {
263
431
  throw new Error('Attester exit authorization file must contain a non-empty JSON array');
264
432
  }
265
-
266
433
  return parsed.map((value, index) => parseAttesterExitAuthorization(value, index));
267
434
  }
268
435
 
436
+ async function readAttesterExitAuthorizationJson(path: string): Promise<unknown[]> {
437
+ const parsed: unknown = JSON.parse(await readFile(path, 'utf-8'));
438
+ if (!Array.isArray(parsed)) {
439
+ throw new Error('Attester exit authorization file must contain a JSON array');
440
+ }
441
+ return parsed;
442
+ }
443
+
269
444
  function isRecord(value: unknown): value is Record<string, unknown> {
270
445
  return typeof value === 'object' && value !== null && !Array.isArray(value);
271
446
  }
@@ -282,7 +457,11 @@ function parseAttesterExitAuthorization(value: unknown, index: number): Attester
282
457
  if (typeof authorization.deadline !== 'string' || !/^\d+$/.test(authorization.deadline)) {
283
458
  throw new Error(`Attester exit authorization ${index} deadline must be a decimal string`);
284
459
  }
285
- if (typeof authorization.signature !== 'string' || !isHex(authorization.signature)) {
460
+ if (
461
+ typeof authorization.signature !== 'string' ||
462
+ !isHex(authorization.signature) ||
463
+ !Signature.isValidString(authorization.signature)
464
+ ) {
286
465
  throw new Error(`Attester exit authorization ${index} has an invalid signature`);
287
466
  }
288
467
 
@@ -293,41 +472,80 @@ function parseAttesterExitAuthorization(value: unknown, index: number): Attester
293
472
  };
294
473
  }
295
474
 
475
+ /**
476
+ * Confirms the RPC is on the requested chain, then checks each imported signature so a bad entry is named rather than
477
+ * reverting the whole batch. Duplicates and deadlines are left to the chain, which with --up-to-limit never looks past
478
+ * the processed prefix.
479
+ */
480
+ async function checkAttesterExitBatch(
481
+ rollup: RollupContract,
482
+ authorizations: AttesterExitAuthorization[],
483
+ rpcUrls: string[],
484
+ chainId: number,
485
+ ) {
486
+ await getL1RpcChainId(rpcUrls, chainId);
487
+ for (const [index, authorization] of authorizations.entries()) {
488
+ await rollup.validateAttesterExitSignature(authorization, index);
489
+ }
490
+ }
491
+
296
492
  /** Relays a batch of attester-signed exits. */
297
493
  export async function initiateWithdrawByAttesterBatch({
298
494
  rpcUrls,
299
495
  chainId,
300
496
  privateKey,
301
497
  mnemonic,
498
+ accountIndex,
499
+ addressIndex,
302
500
  authorizations,
303
501
  upToLimit,
502
+ dryRun = false,
304
503
  rollupAddress,
305
504
  log,
306
505
  debugLogger,
307
- }: Omit<RollupCommandArgs, 'withdrawerAddress'> &
506
+ }: Omit<RollupCommandArgs, 'withdrawerAddress' | 'mnemonic' | 'privateKey'> &
507
+ SignerAccountArgs &
308
508
  LoggerArgs & {
309
509
  authorizations: AttesterExitAuthorization[];
310
510
  upToLimit: boolean;
511
+ dryRun?: boolean;
311
512
  }) {
312
- const account = getAccount(privateKey, mnemonic);
313
- const chain = createEthereumChain(rpcUrls, chainId);
314
- const client = createExtendedL1Client(rpcUrls, account, chain.chainInfo);
513
+ if (dryRun) {
514
+ assertValidChainId(chainId);
515
+ // A dry run needs no key, but check any signer options given so the same command can later run without --dry-run.
516
+ if (privateKey || mnemonic || accountIndex !== undefined || addressIndex !== undefined) {
517
+ getSignerAccount({ privateKey, mnemonic, accountIndex, addressIndex });
518
+ }
519
+ const client = getPublicClient({ l1RpcUrls: rpcUrls, l1ChainId: chainId });
520
+ const rollup = new RollupContract(client, rollupAddress);
521
+ await checkAttesterExitBatch(rollup, authorizations, rpcUrls, chainId);
522
+ const processedCount = await rollup.simulateAttesterExitBatch(authorizations, upToLimit);
523
+ log(
524
+ `Dry run: would process ${processedCount} of ${authorizations.length} attester exit authorizations at current chain state.`,
525
+ );
526
+ logRemainingAttesterExits(log, processedCount, authorizations.length);
527
+ return;
528
+ }
529
+ const account = getSignerAccount({ privateKey, mnemonic, accountIndex, addressIndex });
530
+ const client = createExtendedL1Client(rpcUrls, account, createEthereumChain(rpcUrls, chainId).chainInfo);
315
531
  const rollup = new RollupContract(client, rollupAddress);
532
+ await checkAttesterExitBatch(rollup, authorizations, rpcUrls, chainId);
316
533
  const l1TxUtils = createL1TxUtils(client, { logger: debugLogger });
317
- const { receipt } = upToLimit
318
- ? await rollup.initiateWithdrawByAttesterBatchUpToLimit(l1TxUtils, authorizations)
319
- : await rollup.initiateWithdrawByAttesterBatch(l1TxUtils, authorizations);
320
-
321
- if (receipt.status !== 'success') {
322
- throw new Error(`Attester exit batch reverted: ${receipt.transactionHash}`);
323
- }
324
-
325
- log(`Submitted ${authorizations.length} attester exit authorizations. Transaction hash: ${receipt.transactionHash}`);
326
- if (upToLimit) {
327
- log('The rollup processed the largest permitted prefix of the authorization list.');
328
- }
329
- debugLogger.info('Attester exit batch submitted', {
534
+ const { receipt, processedCount, remainingCount } = await rollup.submitAttesterExitBatch(
535
+ l1TxUtils,
536
+ authorizations,
537
+ upToLimit,
538
+ );
539
+ log(
540
+ `Processed ${processedCount} of ${authorizations.length} attester exit authorizations. Transaction hash: ${receipt.transactionHash}`,
541
+ );
542
+ logRemainingAttesterExits(log, processedCount, authorizations.length);
543
+ debugLogger.info('Attester exit batch processed', {
330
544
  authorizationCount: authorizations.length,
545
+ processedCount,
546
+ remainingCount,
547
+ remainingStartIndex: remainingCount > 0 ? processedCount : undefined,
548
+ remainingEndIndexExclusive: remainingCount > 0 ? authorizations.length : undefined,
331
549
  upToLimit,
332
550
  rollup: rollupAddress.toString(),
333
551
  transactionHash: receipt.transactionHash,
@@ -341,12 +559,18 @@ export async function initiateWithdrawByAttester({
341
559
  chainId,
342
560
  privateKey,
343
561
  mnemonic,
562
+ accountIndex,
563
+ addressIndex,
344
564
  attesterAddress,
345
565
  rollupAddress,
346
566
  log,
347
567
  debugLogger,
348
- }: Omit<RollupCommandArgs, 'withdrawerAddress'> & LoggerArgs & { attesterAddress: EthAddress }) {
349
- const account = getAccount(privateKey, mnemonic);
568
+ }: Omit<RollupCommandArgs, 'withdrawerAddress' | 'mnemonic' | 'privateKey'> &
569
+ SignerAccountArgs &
570
+ LoggerArgs & {
571
+ attesterAddress: EthAddress;
572
+ }) {
573
+ const account = getSignerAccount({ privateKey, mnemonic, accountIndex, addressIndex });
350
574
  if (account.address.toLowerCase() !== attesterAddress.toString().toLowerCase()) {
351
575
  throw new Error('The transaction signer must match the attester address');
352
576
  }
@@ -454,6 +678,15 @@ export async function debugRollup({ rpcUrls, chainId, rollupAddress, log }: Roll
454
678
  log(`Proposer NOW: ${proposer.toString()}`);
455
679
  }
456
680
 
681
+ function logRemainingAttesterExits(log: LogFn, processedCount: number, authorizationCount: number) {
682
+ const remainingCount = authorizationCount - processedCount;
683
+ log(
684
+ remainingCount > 0
685
+ ? `Remaining authorizations: ${remainingCount}. Zero-based JSON array indices ${processedCount} through ${authorizationCount - 1} (inclusive).`
686
+ : 'Remaining authorizations: 0 (none).',
687
+ );
688
+ }
689
+
457
690
  function makeDualLog(log: LogFn, debugLogger: Logger) {
458
691
  return (msg: string) => {
459
692
  log(msg);
@@ -461,6 +694,40 @@ function makeDualLog(log: LogFn, debugLogger: Logger) {
461
694
  };
462
695
  }
463
696
 
697
+ /** `createEthereumChain` maps a falsy chain ID to Anvil, so reject it before a chain is built from it. */
698
+ function assertValidChainId(chainId: number) {
699
+ if (!Number.isSafeInteger(chainId) || chainId <= 0) {
700
+ throw new Error('Chain ID must be a positive safe integer');
701
+ }
702
+ }
703
+
704
+ function getSignerAccount({ privateKey, mnemonic, accountIndex, addressIndex }: SignerAccountArgs) {
705
+ if (Boolean(privateKey) === Boolean(mnemonic)) {
706
+ throw new Error('Provide either a private key or a mnemonic for the signer');
707
+ }
708
+ if (privateKey) {
709
+ if (accountIndex !== undefined || addressIndex !== undefined) {
710
+ throw new Error('Account and address indices require a mnemonic');
711
+ }
712
+ return getAccount(privateKey, undefined);
713
+ }
714
+ const selectedAccountIndex = accountIndex ?? 0;
715
+ const selectedAddressIndex = addressIndex ?? 0;
716
+ if (
717
+ !Number.isSafeInteger(selectedAccountIndex) ||
718
+ selectedAccountIndex < 0 ||
719
+ !Number.isSafeInteger(selectedAddressIndex) ||
720
+ selectedAddressIndex < 0
721
+ ) {
722
+ throw new Error('Account and address indices must be non-negative safe integers');
723
+ }
724
+ const derivedKey = deriveEthAttester(mnemonic!, selectedAccountIndex, selectedAddressIndex);
725
+ if (typeof derivedKey !== 'string') {
726
+ throw new Error('Expected a local signer private key');
727
+ }
728
+ return privateKeyToAccount(derivedKey);
729
+ }
730
+
464
731
  function getAccount(privateKey: string | undefined, mnemonic: string | undefined) {
465
732
  if (!privateKey && !mnemonic) {
466
733
  throw new Error('Either privateKey or mnemonic must be provided to create a wallet client');
@@ -58,6 +58,8 @@ export const devnetConfig = {
58
58
  SPONSORED_FPC: true,
59
59
  TRANSACTIONS_DISABLED: false,
60
60
  SEQ_MAX_TX_PER_BLOCK: 18,
61
+ SEQ_MAX_TX_PER_CHECKPOINT: 36,
62
+ VALIDATOR_MAX_TX_PER_CHECKPOINT: 72,
61
63
  PROVER_REAL_PROOFS: false,
62
64
  PXE_PROVER_ENABLED: false,
63
65
  SYNC_SNAPSHOTS_URLS: '',
@@ -114,6 +116,8 @@ export const testnetConfig = {
114
116
  SEQ_BLOCK_DURATION_MS: 6000,
115
117
  MAX_BLOCKS_PER_CHECKPOINT: 10,
116
118
  CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS: 12,
119
+ SEQ_MAX_TX_PER_CHECKPOINT: 72,
120
+ VALIDATOR_MAX_TX_PER_CHECKPOINT: 144,
117
121
  DATA_STORE_MAP_SIZE_KB: 134217728,
118
122
  ARCHIVER_STORE_MAP_SIZE_KB: 1073741824,
119
123
  NOTE_HASH_TREE_MAP_SIZE_KB: 1073741824,
@@ -140,7 +144,6 @@ export const testnetConfig = {
140
144
  TEST_ACCOUNTS: false,
141
145
  SPONSORED_FPC: false,
142
146
  TRANSACTIONS_DISABLED: false,
143
- SEQ_MAX_TX_PER_CHECKPOINT: 72,
144
147
  PROVER_REAL_PROOFS: true,
145
148
  P2P_MAX_PENDING_TX_COUNT: 1000,
146
149
  P2P_TX_POOL_DELETE_TXS_AFTER_REORG: true,
@@ -182,6 +185,8 @@ export const mainnetConfig = {
182
185
  SEQ_BLOCK_DURATION_MS: 6000,
183
186
  MAX_BLOCKS_PER_CHECKPOINT: 10,
184
187
  CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS: 12,
188
+ SEQ_MAX_TX_PER_CHECKPOINT: 72,
189
+ VALIDATOR_MAX_TX_PER_CHECKPOINT: 144,
185
190
  DATA_STORE_MAP_SIZE_KB: 134217728,
186
191
  ARCHIVER_STORE_MAP_SIZE_KB: 1073741824,
187
192
  NOTE_HASH_TREE_MAP_SIZE_KB: 1073741824,
@@ -216,7 +221,6 @@ export const mainnetConfig = {
216
221
  TEST_ACCOUNTS: false,
217
222
  SPONSORED_FPC: false,
218
223
  TRANSACTIONS_DISABLED: false,
219
- SEQ_MAX_TX_PER_CHECKPOINT: 72,
220
224
  PROVER_REAL_PROOFS: true,
221
225
  BLOB_ALLOW_EMPTY_SOURCES: true,
222
226
  P2P_MAX_PENDING_TX_COUNT: 1000,
@@ -1,4 +1,5 @@
1
1
  export * from './cached_fetch.js';
2
2
  export * from './chain_l2_config.js';
3
+ export * from './generated/networks.js';
3
4
  export * from './get_l1_config.js';
4
5
  export * from './network_config.js';