diff --git a/src/commands/snapshot.ts b/src/commands/snapshot.ts index 2ed4d6f..0168815 100644 --- a/src/commands/snapshot.ts +++ b/src/commands/snapshot.ts @@ -57,7 +57,7 @@ snapshotCli.command("build", { options: snapshotOptions, async run(c) { const snapshotId = c.options.id ?? DEFAULT_SNAPSHOT_ID; - verifySnapshotSemanticState(CONFIG_DIR, RPCS); + await verifySnapshotSemanticState(CONFIG_DIR, RPCS); stopRuntime({ composeFile: COMPOSE_FILE, projectName: "arbitrum-testnode", @@ -74,7 +74,7 @@ snapshotCli.command("build", { }, RPCS, ); - verifySnapshotSemanticState(CONFIG_DIR, RPCS); + await verifySnapshotSemanticState(CONFIG_DIR, RPCS); return { success: true, snapshotId, @@ -104,7 +104,7 @@ snapshotCli.command("restore", { }, RPCS, ); - verifySnapshotSemanticState(CONFIG_DIR, RPCS); + await verifySnapshotSemanticState(CONFIG_DIR, RPCS); return { success: true, snapshotId, @@ -124,7 +124,7 @@ snapshotCli.command("verify", { await waitForRpc(RPCS.l1, 1_000, 100); await waitForRpc(RPCS.l2, 1_000, 100); await waitForRpc(RPCS.l3, 1_000, 100); - verifySnapshotSemanticState(CONFIG_DIR, RPCS); + await verifySnapshotSemanticState(CONFIG_DIR, RPCS); semanticState = "verified"; } catch { semanticState = "skipped"; diff --git a/src/token-bridge.ts b/src/token-bridge.ts index 2c23b38..71e2c17 100644 --- a/src/token-bridge.ts +++ b/src/token-bridge.ts @@ -1,9 +1,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; +import { createTokenBridge, createTokenBridgeFetchTokenBridgeContracts } from "@arbitrum/chain-sdk"; import type { Address } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { accounts } from "./accounts.js"; import { + type BridgeUiConfigFile, ZERO_ADDRESS, createChainSpec, createParentDeployment, @@ -19,27 +21,13 @@ import { execOrThrow } from "./exec.js"; import { arbOwnerAbi, getBalanceWei as getBalanceWeiRpc, + publicClient, readContractOrZero, rollupAbi, walletClient, } from "./rpc.js"; const ARB_OWNER = "0x0000000000000000000000000000000000000070" as const; -function getAdminCliEntry(): string { - const entry = process.env["ARBITRUM_ADMIN_CLI_ENTRY"]; - if (!entry) { - throw new Error("ARBITRUM_ADMIN_CLI_ENTRY env var is required"); - } - return entry; -} -const ADMIN_CLI_NODE_BIN = (() => { - if (process.env["ARBITRUM_ADMIN_NODE_BIN"]) { - return process.env["ARBITRUM_ADMIN_NODE_BIN"]; - } - const node20Path = `${process.env["HOME"] ?? ""}/.nvm/versions/node/v20.19.0/bin/node`; - return existsSync(node20Path) ? node20Path : "node"; -})(); -const NODE20_BIN_DIR = ADMIN_CLI_NODE_BIN === "node" ? null : dirname(ADMIN_CLI_NODE_BIN); const LOCAL_TOKEN_BRIDGE_DIR = process.env["TOKEN_BRIDGE_LOCAL_DIR"] ?? resolve(import.meta.dirname, "../../token-bridge-contracts"); @@ -52,11 +40,14 @@ const PORTAL_LOCAL_NETWORK_PATH = import.meta.dirname, "../../arbitrum-portal/packages/arb-token-bridge-ui/src/util/networksNitroTestnode.generated.json", ); -const BRIDGE_DEPLOY_TIMEOUT_MS = 300_000; const FUNDING_RESERVE_WEI = 1n * 10n ** 18n; const TOKENBRIDGE_DEPLOYER_TARGET_L1_WEI = 100n * 10n ** 18n; const TOKENBRIDGE_DEPLOYER_TARGET_L2_WEI = 100n * 10n ** 18n; const TOKENBRIDGE_DEPLOYER_TARGET_L3_WEI = 10n * 10n ** 18n; +const TOKEN_BRIDGE_TX_GAS_LIMIT = 6_000_000n; +const TOKEN_BRIDGE_RETRYABLE_GAS_LIMIT = 20_000_000n; +const TOKEN_BRIDGE_RETRYABLE_SUBMISSION_COST = 4_000_000_000_000n; +const WETH_GATEWAY_RETRYABLE_GAS_LIMIT = 100_000n; export interface ComposeContext { composeFile: string; @@ -92,6 +83,26 @@ interface L2L3NetworkFile { }; } +interface TokenBridgeContracts { + parentChainContracts: { + router: Address; + standardGateway: Address; + customGateway: Address; + wethGateway: Address; + weth: Address; + multicall: Address; + }; + orbitChainContracts: { + router: Address; + standardGateway: Address; + customGateway: Address; + wethGateway: Address; + weth: Address; + multicall: Address; + proxyAdmin: Address; + }; +} + export function parseTokenBridgeCreatorAddress(output: string): string { const logicMatch = output.match( /L1AtomicTokenBridgeCreator created at address:\s+(0x[a-fA-F0-9]{40})/, @@ -120,10 +131,6 @@ export function parseTokenBridgeCreatorAddress(output: string): string { return creatorAddress; } -function runAdminCli(args: string[], timeoutMs = BRIDGE_DEPLOY_TIMEOUT_MS): string { - return execOrThrow(ADMIN_CLI_NODE_BIN, [getAdminCliEntry(), ...args], { timeout: timeoutMs }); -} - async function getBalanceWei(address: Address, rpcUrl: string): Promise { return getBalanceWeiRpc(address, rpcUrl); } @@ -216,7 +223,6 @@ function deployTokenBridgeCreator(params: { const output = execOrThrow( "env", [ - ...(NODE20_BIN_DIR ? [`PATH=${NODE20_BIN_DIR}:${process.env["PATH"] ?? ""}`] : []), `BASECHAIN_RPC=${toHostAccessibleRpcUrl(params.parentRpc)}`, `BASECHAIN_DEPLOYER_KEY=${params.parentKey}`, `BASECHAIN_WETH=${params.parentWeth ?? ""}`, @@ -237,37 +243,121 @@ function waitForCreatorSettlement(seconds = 10): void { execOrThrow("sleep", [String(seconds)], { timeout: (seconds + 1) * 1000 }); } -function deployChildChainFromSpec( - specPath: string, - configDir: string, - privateKey: string, - outputDirName: "l2" | "l3", -): void { - mkdirSync(getAdminOutputDir(configDir, outputDirName), { recursive: true }); - const args = [ - "deploy", - "child", - "--config", - specPath, - "--private-key", - privateKey, - "--yes", - "--output-dir", - getAdminOutputDir(configDir, outputDirName), - ]; +async function deployChildChainFromSpec( + params: BridgeDeployParams & { + parentRpc: string; + childRpc: string; + tokenBridgeCreator: Address; + deployment: ReturnType; + outbox: string; + chainName: string; + chainId: number; + parentChainId: number; + outputDirName: "l2" | "l3"; + }, +): Promise { + mkdirSync(getAdminOutputDir(params.configDir, params.outputDirName), { recursive: true }); + + const nativeTokenAddress = params.deployment["native-token"] as Address | undefined; + const parentChainPublicClient = publicClient(params.parentRpc); + const orbitChainPublicClient = publicClient(params.childRpc); + const deployTokenBridgeContracts = async (): Promise => { + const result = await createTokenBridge({ + rollupOwner: privateKeyToAccount(params.rollupOwnerKey as `0x${string}`).address, + rollupAddress: params.rollupAddress as Address, + rollupDeploymentBlockNumber: BigInt(params.deployment["deployed-at"] ?? 0), + account: privateKeyToAccount(params.parentKey as `0x${string}`), + ...(nativeTokenAddress && nativeTokenAddress !== ZERO_ADDRESS ? { nativeTokenAddress } : {}), + parentChainPublicClient, + orbitChainPublicClient, + tokenBridgeCreatorAddressOverride: params.tokenBridgeCreator, + gasOverrides: { + gasLimit: { + base: TOKEN_BRIDGE_TX_GAS_LIMIT, + }, + }, + retryableGasOverrides: { + maxGasForFactory: { + base: TOKEN_BRIDGE_RETRYABLE_GAS_LIMIT, + }, + maxGasForContracts: { + base: TOKEN_BRIDGE_RETRYABLE_GAS_LIMIT, + }, + maxSubmissionCostForFactory: { + base: TOKEN_BRIDGE_RETRYABLE_SUBMISSION_COST, + }, + maxSubmissionCostForContracts: { + base: TOKEN_BRIDGE_RETRYABLE_SUBMISSION_COST, + }, + }, + setWethGatewayGasOverrides: { + gasLimit: { + base: WETH_GATEWAY_RETRYABLE_GAS_LIMIT, + }, + }, + }); + return result.tokenBridgeContracts; + }; + const fetchExistingTokenBridgeContracts = async (): Promise => + createTokenBridgeFetchTokenBridgeContracts({ + inbox: params.deployment.inbox as Address, + parentChainPublicClient, + tokenBridgeCreatorAddressOverride: params.tokenBridgeCreator, + }); + const tokenBridgeContracts = await deployChildChainWithRecovery({ + deploy: deployTokenBridgeContracts, + fetchExisting: fetchExistingTokenBridgeContracts, + }); + + writeBridgeUiConfig({ + configDir: params.configDir, + outputDirName: params.outputDirName, + chainName: params.chainName, + parentChainId: params.parentChainId, + chainId: params.chainId, + parentRpc: params.parentRpc, + childRpc: params.childRpc, + deployment: params.deployment, + outbox: params.outbox, + tokenBridgeContracts, + }); +} + +async function deployChildChainWithRecovery(input: { + deploy: () => Promise; + fetchExisting: () => Promise; +}): Promise { try { - runAdminCli(args, 600_000); + return await input.deploy(); } catch (error) { - if (!shouldRetryChildDeploy(specPath, error)) { + if (shouldFetchExistingChildDeploy(error)) { + console.warn("[init] Token bridge already exists; loading deployed contract addresses"); + return input.fetchExisting(); + } + if (!shouldRetryChildDeploy(error)) { throw error; } - console.warn("[init] Child deploy wrote partial state; retrying once"); - runAdminCli(args, 600_000); + console.warn("[init] Token bridge deployment wrote partial state; retrying once"); + try { + return await input.deploy(); + } catch (retryError) { + if (shouldFetchExistingChildDeploy(retryError)) { + console.warn( + "[init] Token bridge already exists after retry; loading deployed contract addresses", + ); + return input.fetchExisting(); + } + throw retryError; + } } } -function shouldRetryChildDeploy(specPath: string, error: unknown): boolean { - void specPath; +function shouldFetchExistingChildDeploy(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + return message.includes("already deployed") || message.includes("already included"); +} + +function shouldRetryChildDeploy(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); return ( message.includes("Unexpected status for retryable ticket") || @@ -278,6 +368,63 @@ function shouldRetryChildDeploy(specPath: string, error: unknown): boolean { ); } +function writeBridgeUiConfig(input: { + configDir: string; + outputDirName: "l2" | "l3"; + chainName: string; + parentChainId: number; + chainId: number; + parentRpc: string; + childRpc: string; + deployment: ReturnType; + outbox: string; + tokenBridgeContracts: TokenBridgeContracts; +}): void { + const outputDir = getAdminOutputDir(input.configDir, input.outputDirName); + const parent = input.tokenBridgeContracts.parentChainContracts; + const child = input.tokenBridgeContracts.orbitChainContracts; + const nativeToken = input.deployment["native-token"] ?? ZERO_ADDRESS; + const bridgeUiConfig: BridgeUiConfigFile = { + chainName: input.chainName, + parentChainId: input.parentChainId, + chainId: input.chainId, + rollup: input.deployment.rollup, + parentChainRpc: input.parentRpc, + chainRpc: input.childRpc, + nativeToken, + coreContracts: { + bridge: input.deployment.bridge, + inbox: input.deployment.inbox, + outbox: input.outbox, + rollup: input.deployment.rollup, + sequencerInbox: input.deployment["sequencer-inbox"], + }, + tokenBridge: { + parentChain: { + router: parent.router, + standardGateway: parent.standardGateway, + customGateway: parent.customGateway, + wethGateway: parent.wethGateway, + weth: parent.weth, + multicall: parent.multicall, + }, + chain: { + router: child.router, + standardGateway: child.standardGateway, + customGateway: child.customGateway, + wethGateway: child.wethGateway, + weth: child.weth, + multicall: child.multicall, + proxyAdmin: child.proxyAdmin, + }, + }, + }; + writeFileSync( + join(outputDir, "bridgeUiConfig.json"), + `${JSON.stringify(bridgeUiConfig, null, 2)}\n`, + ); +} + function publishLocalNetworkArtifacts(configDir: string): void { const localNetworkPath = join(configDir, "localNetwork.json"); if (!existsSync(localNetworkPath)) { @@ -364,6 +511,12 @@ export async function deployL1L2TokenBridge(params: BridgeDeployParams): Promise }); waitForCreatorSettlement(); const rollupAddress = params.rollupAddress as Address; + const parentDeployment = createParentDeployment({ + deployment: l2Deployment, + outbox: await readAddressOrZero(rollupAddress, "outbox", parentRpc), + rollupEventInbox: await readAddressOrZero(rollupAddress, "rollupEventInbox", parentRpc), + challengeManager: await readAddressOrZero(rollupAddress, "challengeManager", parentRpc), + }); const spec = createChainSpec({ chainName: "arb-dev-test", chainId: 412346, @@ -371,16 +524,22 @@ export async function deployL1L2TokenBridge(params: BridgeDeployParams): Promise parentRpc, chainRpc: childRpc, tokenBridgeCreator, - parentDeployment: createParentDeployment({ - deployment: l2Deployment, - outbox: await readAddressOrZero(rollupAddress, "outbox", parentRpc), - rollupEventInbox: await readAddressOrZero(rollupAddress, "rollupEventInbox", parentRpc), - challengeManager: await readAddressOrZero(rollupAddress, "challengeManager", parentRpc), - }), + parentDeployment, owner: accounts.l2owner.address, }); writeChainSpecFile(specPath, spec); - deployChildChainFromSpec(specPath, params.configDir, params.rollupOwnerKey, "l2"); + await deployChildChainFromSpec({ + ...params, + parentRpc, + childRpc, + tokenBridgeCreator: tokenBridgeCreator as Address, + deployment: l2Deployment, + outbox: parentDeployment.outbox, + chainName: "arb-dev-test", + chainId: 412346, + parentChainId: 1337, + outputDirName: "l2", + }); writeSdkNetworkFileFromBridgeUiConfig( params.configDir, getAdminOutputDir(params.configDir, "l2"), @@ -395,6 +554,7 @@ export async function deployL2L3TokenBridge(params: BridgeDeployParams): Promise const specPath = getChainSpecPath(params.configDir, "l3"); const parentRpc = toHostAccessibleRpcUrl(params.parentRpc); const childRpc = toHostAccessibleRpcUrl(params.childRpc); + const l3Deployment = readDeploymentArtifact(params.configDir, "l3_deployment.json"); const tokenBridgeCreator = deployTokenBridgeCreator({ compose: params.compose, parentRpc: params.parentRpc, @@ -403,6 +563,12 @@ export async function deployL2L3TokenBridge(params: BridgeDeployParams): Promise }); waitForCreatorSettlement(); const rollupAddress = params.rollupAddress as Address; + const parentDeployment = createParentDeployment({ + deployment: l3Deployment, + outbox: await readAddressOrZero(rollupAddress, "outbox", parentRpc), + rollupEventInbox: await readAddressOrZero(rollupAddress, "rollupEventInbox", parentRpc), + challengeManager: await readAddressOrZero(rollupAddress, "challengeManager", parentRpc), + }); const spec = createChainSpec({ chainName: "orbit-dev-test", chainId: 333333, @@ -410,22 +576,26 @@ export async function deployL2L3TokenBridge(params: BridgeDeployParams): Promise parentRpc, chainRpc: childRpc, tokenBridgeCreator, - parentDeployment: createParentDeployment({ - deployment: readDeploymentArtifact(params.configDir, "l3_deployment.json"), - outbox: await readAddressOrZero(rollupAddress, "outbox", parentRpc), - rollupEventInbox: await readAddressOrZero(rollupAddress, "rollupEventInbox", parentRpc), - challengeManager: await readAddressOrZero(rollupAddress, "challengeManager", parentRpc), - }), + parentDeployment, ownership: { - addChainOwners: [ - readDeploymentArtifact(params.configDir, "l3_deployment.json")["upgrade-executor"], - ], + addChainOwners: [l3Deployment["upgrade-executor"]], removeDeployer: true, }, owner: accounts.l3owner.address, }); writeChainSpecFile(specPath, spec); - deployChildChainFromSpec(specPath, params.configDir, params.rollupOwnerKey, "l3"); + await deployChildChainFromSpec({ + ...params, + parentRpc, + childRpc, + tokenBridgeCreator: tokenBridgeCreator as Address, + deployment: l3Deployment, + outbox: parentDeployment.outbox, + chainName: "orbit-dev-test", + chainId: 333333, + parentChainId: 412346, + outputDirName: "l3", + }); writeSdkNetworkFileFromBridgeUiConfig( params.configDir, getAdminOutputDir(params.configDir, "l3"), diff --git a/test/token-bridge.test.ts b/test/token-bridge.test.ts index 0527f7e..6d44453 100644 --- a/test/token-bridge.test.ts +++ b/test/token-bridge.test.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +import { createTokenBridge, createTokenBridgeFetchTokenBridgeContracts } from "@arbitrum/chain-sdk"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as execModule from "../src/exec.js"; import { @@ -9,42 +10,19 @@ import { parseTokenBridgeCreatorAddress, } from "../src/token-bridge.js"; -vi.mock("../src/exec.js", () => ({ - execOrThrow: vi.fn(), -})); - -function isTokenBridgeCreatorDeploy(command: string, args: string[]): boolean { - return ( - (command === "docker" || command === "env") && args.includes("deploy:token-bridge-creator") - ); -} - -const BRIDGE_UI_CONFIG_FIXTURE = { - chainName: "orbit-dev-test", - parentChainId: 412346, - chainId: 333333, - rollup: "0x1111111111111111111111111111111111111111", - parentChainRpc: "http://127.0.0.1:8547", - chainRpc: "http://127.0.0.1:8549", - nativeToken: "0x0000000000000000000000000000000000000000", - coreContracts: { - bridge: "0x3333333333333333333333333333333333333333", - inbox: "0x2222222222222222222222222222222222222222", - outbox: "0x7777777777777777777777777777777777777777", - rollup: "0x1111111111111111111111111111111111111111", - sequencerInbox: "0x4444444444444444444444444444444444444444", - }, - tokenBridge: { - parentChain: { +const mocks = vi.hoisted(() => ({ + createTokenBridge: vi.fn(), + createTokenBridgeFetchTokenBridgeContracts: vi.fn(), + tokenBridgeContracts: { + parentChainContracts: { router: "0xa111111111111111111111111111111111111111", standardGateway: "0xa222222222222222222222222222222222222222", customGateway: "0xa333333333333333333333333333333333333333", wethGateway: "0xa444444444444444444444444444444444444444", weth: "0xa555555555555555555555555555555555555555", multicall: "0xa666666666666666666666666666666666666666", - proxyAdmin: "0xa777777777777777777777777777777777777777", }, - chain: { + orbitChainContracts: { router: "0xb111111111111111111111111111111111111111", standardGateway: "0xb222222222222222222222222222222222222222", customGateway: "0xb333333333333333333333333333333333333333", @@ -52,22 +30,25 @@ const BRIDGE_UI_CONFIG_FIXTURE = { weth: "0xb555555555555555555555555555555555555555", multicall: "0xb666666666666666666666666666666666666666", proxyAdmin: "0xb777777777777777777777777777777777777777", + beaconProxyFactory: "0xb888888888888888888888888888888888888888", + upgradeExecutor: "0xb999999999999999999999999999999999999999", }, }, -}; +})); -function writeBridgeUiConfigAndReturn(args: string[]): string { - const outputDirIndex = args.indexOf("--output-dir"); - const outputDir = args[outputDirIndex + 1]; - if (!outputDir) { - throw new Error("missing --output-dir path"); - } - fs.mkdirSync(outputDir, { recursive: true }); - fs.writeFileSync( - path.join(outputDir, "bridgeUiConfig.json"), - JSON.stringify(BRIDGE_UI_CONFIG_FIXTURE), +vi.mock("@arbitrum/chain-sdk", () => ({ + createTokenBridge: mocks.createTokenBridge, + createTokenBridgeFetchTokenBridgeContracts: mocks.createTokenBridgeFetchTokenBridgeContracts, +})); + +vi.mock("../src/exec.js", () => ({ + execOrThrow: vi.fn(), +})); + +function isTokenBridgeCreatorDeploy(command: string, args: string[]): boolean { + return ( + (command === "docker" || command === "env") && args.includes("deploy:token-bridge-creator") ); - return JSON.stringify({ ok: true }); } vi.mock("../src/rpc.js", () => ({ @@ -86,7 +67,12 @@ vi.mock("../src/rpc.js", () => ({ return "0x0000000000000000000000000000000000000000"; } }), - publicClient: vi.fn(), + publicClient: vi.fn((rpcUrl: string) => ({ + rpcUrl, + chain: { + id: rpcUrl.includes(":8549") ? 333333 : rpcUrl.includes(":8547") ? 412346 : 1337, + }, + })), walletClient: vi.fn().mockReturnValue({ sendTransaction: vi.fn().mockResolvedValue("0x"), writeContract: vi.fn().mockResolvedValue("0x"), @@ -159,20 +145,20 @@ describe("deployL2L3TokenBridge", () => { let previousSdkPath: string | undefined; let previousPortalPath: string | undefined; - let previousAdminCliEntry: string | undefined; - beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "token-bridge-test-")); vi.clearAllMocks(); + mocks.createTokenBridge.mockResolvedValue({ + tokenBridgeContracts: mocks.tokenBridgeContracts, + }); + mocks.createTokenBridgeFetchTokenBridgeContracts.mockResolvedValue(mocks.tokenBridgeContracts); previousSdkPath = process.env["ARBITRUM_SDK_LOCAL_NETWORK_PATH"]; previousPortalPath = process.env["ARBITRUM_PORTAL_LOCAL_NETWORK_PATH"]; - previousAdminCliEntry = process.env["ARBITRUM_ADMIN_CLI_ENTRY"]; process.env["ARBITRUM_SDK_LOCAL_NETWORK_PATH"] = path.join(tmpDir, "sdk-localNetwork.json"); process.env["ARBITRUM_PORTAL_LOCAL_NETWORK_PATH"] = path.join( tmpDir, "portal-localNetwork.json", ); - process.env["ARBITRUM_ADMIN_CLI_ENTRY"] = "/test/admin-cli/dist/index.cjs"; }); afterEach(() => { @@ -188,12 +174,6 @@ describe("deployL2L3TokenBridge", () => { } else { process.env["ARBITRUM_PORTAL_LOCAL_NETWORK_PATH"] = previousPortalPath; } - if (previousAdminCliEntry === undefined) { - // biome-ignore lint/performance/noDelete: process.env requires delete; assigning undefined stringifies - delete process.env["ARBITRUM_ADMIN_CLI_ENTRY"]; - } else { - process.env["ARBITRUM_ADMIN_CLI_ENTRY"] = previousAdminCliEntry; - } fs.rmSync(tmpDir, { recursive: true, force: true }); }); @@ -216,9 +196,6 @@ describe("deployL2L3TokenBridge", () => { if (isTokenBridgeCreatorDeploy(command, args)) { return "L1TokenBridgeCreator: 0x332Fb35767182F8ac9F9C1405db626105F6694E0"; } - if (command === "node" || command.endsWith("/node")) { - return writeBridgeUiConfigAndReturn(args); - } return ""; }); @@ -251,22 +228,48 @@ describe("deployL2L3TokenBridge", () => { cwd: expect.any(String), }), ); - expect(execOrThrow).toHaveBeenCalledWith( + expect(execOrThrow).not.toHaveBeenCalledWith( expect.stringMatching(/(?:^node$|\/node$)/), - expect.arrayContaining([ - "/test/admin-cli/dist/index.cjs", - "deploy", - "child", - "--config", - path.join(tmpDir, "l2-l3-chain-config.json"), - "--private-key", - "0x2222222222222222222222222222222222222222222222222222222222222222", - "--yes", - "--output-dir", - path.join(tmpDir, "l2-l3-admin"), - ]), + expect.anything(), expect.anything(), ); + expect(createTokenBridge).toHaveBeenCalledWith( + expect.objectContaining({ + rollupAddress: "0x1111111111111111111111111111111111111111", + rollupDeploymentBlockNumber: 44n, + tokenBridgeCreatorAddressOverride: "0x332Fb35767182F8ac9F9C1405db626105F6694E0", + parentChainPublicClient: expect.objectContaining({ + rpcUrl: "http://127.0.0.1:8547", + }), + orbitChainPublicClient: expect.objectContaining({ + rpcUrl: "http://127.0.0.1:8549", + }), + gasOverrides: { + gasLimit: { + base: 6_000_000n, + }, + }, + retryableGasOverrides: { + maxGasForFactory: { + base: 20_000_000n, + }, + maxGasForContracts: { + base: 20_000_000n, + }, + maxSubmissionCostForFactory: { + base: 4_000_000_000_000n, + }, + maxSubmissionCostForContracts: { + base: 4_000_000_000_000n, + }, + }, + setWethGatewayGasOverrides: { + gasLimit: { + base: 100_000n, + }, + }, + }), + ); const l2l3Network = JSON.parse( fs.readFileSync(path.join(tmpDir, "l2l3_network.json"), "utf-8"), @@ -343,4 +346,74 @@ describe("deployL2L3TokenBridge", () => { "0x9999999999999999999999999999999999999999", ); }); + + it("loads existing token bridge contracts when the SDK retry reports an existing deployment", async () => { + fs.writeFileSync( + path.join(tmpDir, "l3_deployment.json"), + JSON.stringify({ + rollup: "0x1111111111111111111111111111111111111111", + inbox: "0x2222222222222222222222222222222222222222", + bridge: "0x3333333333333333333333333333333333333333", + "sequencer-inbox": "0x4444444444444444444444444444444444444444", + "upgrade-executor": "0x5555555555555555555555555555555555555555", + "validator-wallet-creator": "0x6666666666666666666666666666666666666666", + "native-token": "0x0000000000000000000000000000000000000000", + "deployed-at": 44, + }), + ); + const execOrThrow = vi.mocked(execModule.execOrThrow); + execOrThrow.mockImplementation((command, args) => { + if (isTokenBridgeCreatorDeploy(command, args)) { + return "L1TokenBridgeCreator: 0x332Fb35767182F8ac9F9C1405db626105F6694E0"; + } + return ""; + }); + mocks.createTokenBridge + .mockRejectedValueOnce(new Error("Unexpected status for retryable ticket: 0xabc")) + .mockRejectedValueOnce( + new Error( + "Token bridge contracts for Rollup 0x1111111111111111111111111111111111111111 are already deployed", + ), + ); + + await deployL2L3TokenBridge({ + compose: { + composeFile: "/tmp/docker-compose.yaml", + projectName: "arbitrum-testnode", + }, + configDir: tmpDir, + rollupAddress: "0x1111111111111111111111111111111111111111", + rollupOwnerKey: "0x2222222222222222222222222222222222222222222222222222222222222222", + parentRpc: "http://sequencer:8547", + childRpc: "http://l3node:8547", + parentKey: "0x3333333333333333333333333333333333333333333333333333333333333333", + childKey: "0x3333333333333333333333333333333333333333333333333333333333333333", + parentWethOverride: "0x5555555555555555555555555555555555555555", + }); + + expect(createTokenBridge).toHaveBeenCalledTimes(2); + expect(createTokenBridgeFetchTokenBridgeContracts).toHaveBeenCalledWith({ + inbox: "0x2222222222222222222222222222222222222222", + parentChainPublicClient: expect.objectContaining({ + rpcUrl: "http://127.0.0.1:8547", + }), + tokenBridgeCreatorAddressOverride: "0x332Fb35767182F8ac9F9C1405db626105F6694E0", + }); + const l2l3Network = JSON.parse( + fs.readFileSync(path.join(tmpDir, "l2l3_network.json"), "utf-8"), + ) as { + l3Network: { + tokenBridge: { + parentGatewayRouter: string; + childGatewayRouter: string; + }; + }; + }; + expect(l2l3Network.l3Network.tokenBridge.parentGatewayRouter).toBe( + "0xa111111111111111111111111111111111111111", + ); + expect(l2l3Network.l3Network.tokenBridge.childGatewayRouter).toBe( + "0xb111111111111111111111111111111111111111", + ); + }); });