|
| 1 | +import type { AztecNodeService } from '@aztec/aztec-node'; |
| 2 | +import type { AztecNodeConfig } from '@aztec/aztec-node/config'; |
| 3 | +import type { Fr } from '@aztec/aztec.js/fields'; |
| 4 | +import { startAnvil } from '@aztec/ethereum/test'; |
| 5 | +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; |
| 6 | + |
| 7 | +import { foundry } from 'viem/chains'; |
| 8 | + |
| 9 | +import { createLocalNetwork } from '../local-network/local-network.js'; |
| 10 | + |
| 11 | +/** A running in-process local network: an inline Aztec node backed by its own anvil L1. */ |
| 12 | +export interface LocalNetwork extends AsyncDisposable { |
| 13 | + /** Fully-synced Aztec node, ready to serve client requests. */ |
| 14 | + node: AztecNodeService; |
| 15 | + /** RPC URL of the spawned anvil instance. */ |
| 16 | + l1RpcUrl: string; |
| 17 | + /** Chain id used on L1 (foundry's default 31337). */ |
| 18 | + l1ChainId: number; |
| 19 | + /** Stops every process started by the fixture: node and anvil. Also invoked by `await using`. */ |
| 20 | + stop: () => Promise<void>; |
| 21 | +} |
| 22 | + |
| 23 | +/** Options for {@link setupLocalNetwork}. */ |
| 24 | +export interface LocalNetworkOptions { |
| 25 | + /** |
| 26 | + * Addresses that should hold fee juice at genesis. Saves each of these the round-trip of bridging |
| 27 | + * + claiming fee juice before they can pay for gas. |
| 28 | + */ |
| 29 | + fundedAddresses?: AztecAddress[]; |
| 30 | + /** Override the default per-address genesis fee juice granted to {@link fundedAddresses}. */ |
| 31 | + initialAccountFeeJuice?: Fr; |
| 32 | + /** Node config overrides, e.g. `realProofs`, `aztecEpochDuration`, `p2pEnabled`. */ |
| 33 | + config?: Partial<AztecNodeConfig>; |
| 34 | +} |
| 35 | + |
| 36 | +/** |
| 37 | + * Spin up an in-process local network with the given addresses pre-funded. |
| 38 | + * |
| 39 | + * Each call spawns its own anvil on an OS-assigned random port and runs the Aztec node inline via |
| 40 | + * the same {@link createLocalNetwork} codepath that backs `aztec start --local-network` (with the |
| 41 | + * sandbox account/FPC/token setup skipped). Distinct ports let independent suites run in parallel. |
| 42 | + * The caller must `await result.stop()` in its teardown (or hold the result with `await using`). |
| 43 | + * |
| 44 | + * Requires a Foundry toolchain (`anvil`/`forge`), installed via `aztec-up` or `foundryup`. Binaries |
| 45 | + * are located in the standard install directories or on `PATH`; set `$ANVIL_BIN` / `$FORGE_BIN` to |
| 46 | + * pin specific ones. |
| 47 | + */ |
| 48 | +export async function setupLocalNetwork(opts: LocalNetworkOptions = {}): Promise<LocalNetwork> { |
| 49 | + // `--port 0` → anvil binds an ephemeral port that `startAnvil` reads back, so parallel suites |
| 50 | + // never collide on a fixed port. |
| 51 | + const { rpcUrl, stop: stopAnvil } = await startAnvil({ port: 0 }); |
| 52 | + |
| 53 | + try { |
| 54 | + const { node, stop: stopNode } = await createLocalNetwork( |
| 55 | + { |
| 56 | + ...opts.config, |
| 57 | + l1RpcUrls: [rpcUrl], |
| 58 | + testAccounts: false, |
| 59 | + prefundAddresses: (opts.fundedAddresses ?? []).map(a => a.toString()), |
| 60 | + initialAccountFeeJuice: opts.initialAccountFeeJuice, |
| 61 | + }, |
| 62 | + () => {}, |
| 63 | + ); |
| 64 | + |
| 65 | + // Stop the node before anvil (its teardown still talks to L1); the finally guarantees anvil is |
| 66 | + // reaped even if node shutdown throws. |
| 67 | + const stop = async () => { |
| 68 | + try { |
| 69 | + await stopNode(); |
| 70 | + } finally { |
| 71 | + await stopAnvil(); |
| 72 | + } |
| 73 | + }; |
| 74 | + |
| 75 | + return { |
| 76 | + node, |
| 77 | + l1RpcUrl: rpcUrl, |
| 78 | + l1ChainId: foundry.id, |
| 79 | + stop, |
| 80 | + [Symbol.asyncDispose]: stop, |
| 81 | + }; |
| 82 | + } catch (err) { |
| 83 | + await stopAnvil(); |
| 84 | + throw err; |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +/** |
| 89 | + * Min-fee padding multiplier for test wallets whose txs may mine well after their fee estimate. |
| 90 | + * The automine sequencer builds one block per tx and advances L1 time in big jumps, and proposer |
| 91 | + * pipelining evolves the fee-asset price across the build/publish gap (~20x observed in CI), so the |
| 92 | + * network's congestion base fee can swing sharply between the wallet's fee estimate and the block |
| 93 | + * the tx actually lands in. The default wallet padding isn't enough and trips |
| 94 | + * `maxFeesPerGas.feePerL2Gas must be >= gasFees.feePerL2Gas`. Apply via |
| 95 | + * `wallet.setMinFeePadding(TEST_FEE_PADDING)` on every test wallet that sends txs. |
| 96 | + */ |
| 97 | +export const TEST_FEE_PADDING = 30; |
0 commit comments