From c1f4a70ce98539ebf12d865b79e4f998603bc968 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 14:27:42 +0200 Subject: [PATCH 01/15] Add deposit & withdraw for OP. --- packages/cli/docs/bridge.md | 171 +++++++++++++ packages/cli/package.json | 3 + .../cli/src/commands/bridge/deposit.test.ts | 105 ++++++++ packages/cli/src/commands/bridge/deposit.ts | 148 +++++++++++ .../src/commands/bridge/withdraw-finalize.ts | 146 +++++++++++ .../cli/src/commands/bridge/withdraw-init.ts | 84 +++++++ .../cli/src/commands/bridge/withdraw-prove.ts | 146 +++++++++++ .../src/commands/bridge/withdraw-status.ts | 128 ++++++++++ .../cli/src/commands/bridge/withdraw.test.ts | 235 ++++++++++++++++++ packages/cli/src/utils/bridge.test.ts | 68 +++++ packages/cli/src/utils/bridge.ts | 216 ++++++++++++++++ 11 files changed, 1450 insertions(+) create mode 100644 packages/cli/docs/bridge.md create mode 100644 packages/cli/src/commands/bridge/deposit.test.ts create mode 100644 packages/cli/src/commands/bridge/deposit.ts create mode 100644 packages/cli/src/commands/bridge/withdraw-finalize.ts create mode 100644 packages/cli/src/commands/bridge/withdraw-init.ts create mode 100644 packages/cli/src/commands/bridge/withdraw-prove.ts create mode 100644 packages/cli/src/commands/bridge/withdraw-status.ts create mode 100644 packages/cli/src/commands/bridge/withdraw.test.ts create mode 100644 packages/cli/src/utils/bridge.test.ts create mode 100644 packages/cli/src/utils/bridge.ts diff --git a/packages/cli/docs/bridge.md b/packages/cli/docs/bridge.md new file mode 100644 index 0000000000..b7a894c012 --- /dev/null +++ b/packages/cli/docs/bridge.md @@ -0,0 +1,171 @@ +# Bridge Commands + +Bridge CELO tokens between Ethereum (L1) and Celo (L2) using the OP Stack bridge. + +## Overview + +The bridge commands allow you to move CELO between Ethereum (Layer 1) and Celo (Layer 2): + +- **Deposit** (L1 → L2): Move CELO from Ethereum to Celo. Takes ~15 minutes. +- **Withdrawal** (L2 → L1): Move CELO from Celo to Ethereum. Takes ~7 days due to the challenge period. + +## Prerequisites + +- A wallet with CELO on the source chain +- RPC URLs for both L1 (Ethereum) and L2 (Celo) +- A private key or Ledger hardware wallet for signing transactions + +## Deposit: Ethereum (L1) → Celo (L2) + +Deposits are simple — one command, ~15 minute wait. + +### Command + +```bash +celocli bridge:deposit \ + --from 0xYourAddress \ + --to 0xRecipientOnL2 \ + --value 1000000000000000000 \ + --network mainnet \ + --l1RpcUrl https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \ + -k 0xYourPrivateKey +``` + +### Flags + +| Flag | Required | Description | +|------|----------|-------------| +| `--from` | Yes | Your address on L1 (sender) | +| `--to` | No | Recipient address on L2 (defaults to `--from`) | +| `--value` | Yes | Amount in wei (1 CELO = 1000000000000000000) | +| `--network` | Yes | `mainnet` or `sepolia` | +| `--l1RpcUrl` | Yes | Ethereum RPC URL | +| `--gaslimit` | No | L2 gas limit (default: 100000) | +| `-k` / `--privateKey` | Yes* | Private key for signing | +| `--useLedger` | Yes* | Use Ledger hardware wallet | + +*One of `--privateKey` or `--useLedger` is required. + +### What happens + +1. The CLI retrieves the CELO token address on L1 +2. Approves the bridge contract to spend your CELO +3. Submits the deposit transaction +4. Your CELO appears on L2 in ~15 minutes + +## Withdrawal: Celo (L2) → Ethereum (L1) + +Withdrawals are a multi-step process due to the OP Stack's 7-day security challenge period. + +### Step 1: Initiate the Withdrawal + +```bash +celocli bridge:withdraw-init \ + --from 0xYourL2Address \ + --to 0xYourL1Address \ + --value 1000000000000000000 \ + --network mainnet \ + -n mainnet \ + -k 0xYourPrivateKey +``` + +**Save the transaction hash from the output!** You'll need it for all subsequent steps. + +### Step 2: Submit the Proof (~1 hour after Step 1) + +Wait about 1 hour for the L2 output to be published on L1, then submit the proof: + +```bash +celocli bridge:withdraw-prove \ + --txHash 0xYourL2TxHash \ + --from 0xYourAddress \ + --network mainnet \ + --l1RpcUrl https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \ + -n mainnet \ + -k 0xYourPrivateKey +``` + +This command will wait for the proof to become available if it isn't ready yet. + +### Step 3: Wait for the Challenge Period (7 days) + +After proving, you must wait 7 days for the security challenge period to pass. You can check the status at any time: + +```bash +celocli bridge:withdraw-status \ + --txHash 0xYourL2TxHash \ + --network mainnet \ + --l1RpcUrl https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \ + -n mainnet +``` + +The status command will tell you exactly where your withdrawal stands and what to do next. + +### Step 4: Finalize and Claim (after 7 days) + +Once the challenge period has passed: + +```bash +celocli bridge:withdraw-finalize \ + --txHash 0xYourL2TxHash \ + --from 0xYourAddress \ + --network mainnet \ + --l1RpcUrl https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \ + -n mainnet \ + -k 0xYourPrivateKey +``` + +Your CELO will be transferred to your L1 address. + +## Checking Withdrawal Status + +You can check the status of any withdrawal at any point: + +```bash +celocli bridge:withdraw-status \ + --txHash 0xYourL2TxHash \ + --network mainnet \ + --l1RpcUrl https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \ + -n mainnet +``` + +### Possible Statuses + +| Status | Meaning | Next Action | +|--------|---------|-------------| +| **Waiting to Prove** | Withdrawal initiated, proof not yet available | Wait ~1 hour | +| **Ready to Prove** | Proof available | Run `bridge:withdraw-prove` | +| **Waiting to Finalize** | Proof submitted, challenge period in progress | Wait (up to 7 days) | +| **Ready to Finalize** | Challenge period passed | Run `bridge:withdraw-finalize` | +| **Finalized** | Complete | Nothing — funds are on L1 | + +## Network Support + +| Network | L1 | L2 | +|---------|----|----| +| `mainnet` | Ethereum Mainnet | Celo Mainnet | +| `sepolia` | Ethereum Sepolia | Celo Alfajores | + +## Using with Ledger + +All bridge commands support Ledger hardware wallets. Replace `-k 0xYourPrivateKey` with `--useLedger`: + +```bash +celocli bridge:deposit \ + --from 0xYourAddress \ + --value 1000000000000000000 \ + --network mainnet \ + --l1RpcUrl https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \ + --useLedger +``` + +## Common Issues + +**"Bridge commands require --privateKey or --useLedger"** +You must provide a signing method. Add `-k 0xYourKey` or `--useLedger`. + +**"Cannot finalize: The 7-day challenge period has not passed yet"** +The challenge period hasn't elapsed. Run `bridge:withdraw-status` to check progress. + +**"Cannot finalize: The withdrawal has not been proven yet"** +You need to submit the proof first with `bridge:withdraw-prove`. diff --git a/packages/cli/package.json b/packages/cli/package.json index 086e160356..6685e4e615 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -125,6 +125,9 @@ "account": { "description": "Manage your account, keys, and metadata" }, + "bridge": { + "description": "Bridge CELO between Ethereum (L1) and Celo (L2)" + }, "config": { "description": "Configure CLI options which persist across commands" }, diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts new file mode 100644 index 0000000000..07421d828b --- /dev/null +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -0,0 +1,105 @@ +import BridgeDeposit from './deposit' + +// Inline testLocally to avoid importing cliUtils which pulls in @celo/dev-utils +async function testLocally(command: any, argv: string[]) { + if (argv.includes('--node')) { + return command.run(argv) + } + const extendedArgv = command.flags?.node ? [...argv, '--node', 'local'] : argv + return command.run(extendedArgv) +} + +process.env.NO_SYNCCHECK = 'true' + +jest.setTimeout(15000) + +describe('bridge:deposit', () => { + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}) + jest.spyOn(console, 'error').mockImplementation(() => {}) + jest.spyOn(console, 'info').mockImplementation(() => {}) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('requires --from flag', async () => { + await expect( + testLocally(BridgeDeposit, [ + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('requires --value flag', async () => { + await expect( + testLocally(BridgeDeposit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('requires --network flag', async () => { + await expect( + testLocally(BridgeDeposit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('requires --l1RpcUrl flag', async () => { + await expect( + testLocally(BridgeDeposit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('rejects invalid network value', async () => { + await expect( + testLocally(BridgeDeposit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('rejects invalid address format', async () => { + await expect( + testLocally(BridgeDeposit, [ + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow('is not a valid address') + }) + + it('requires signing method (privateKey or useLedger)', async () => { + await expect( + testLocally(BridgeDeposit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + ]) + ).rejects.toThrow() + }) +}) diff --git a/packages/cli/src/commands/bridge/deposit.ts b/packages/cli/src/commands/bridge/deposit.ts new file mode 100644 index 0000000000..35cbcbec82 --- /dev/null +++ b/packages/cli/src/commands/bridge/deposit.ts @@ -0,0 +1,148 @@ +import { Flags, ux } from '@oclif/core' +import { createPublicClient, createWalletClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' +import { CustomFlags } from '../../utils/command' +import { + BRIDGE_CONFIG, + SYSTEM_CONFIG_ABI, + ERC20_APPROVE_ABI, + OPTIMISM_PORTAL_DEPOSIT_ABI, + validateNetwork, + type BridgeNetwork, +} from '../../utils/bridge' + +export default class BridgeDeposit extends BaseCommand { + static description = + 'Deposit CELO from Ethereum (L1) to Celo (L2). This bridges your CELO tokens to the Celo L2 network.' + + static examples = [ + 'bridge:deposit --from 0xYOUR_ADDRESS --to 0xRECIPIENT --value 1000000000000000000 --network mainnet --l1RpcUrl https://eth-mainnet.example.com -k 0xPRIVATE_KEY', + 'bridge:deposit --from 0xYOUR_ADDRESS --value 1000000000000000000 --network sepolia --l1RpcUrl https://eth-sepolia.example.com -k 0xPRIVATE_KEY', + ] + + static flags = { + ...BaseCommand.flags, + from: CustomFlags.address({ + required: true, + description: 'Address of the sender on L1', + }), + to: CustomFlags.address({ + description: 'Address of the recipient on L2 (defaults to sender address)', + }), + value: CustomFlags.bigint({ + required: true, + description: 'Amount of CELO to deposit (in wei)', + }), + network: Flags.string({ + required: true, + options: ['mainnet', 'sepolia'], + description: 'Network to bridge on (mainnet or sepolia)', + }), + l1RpcUrl: Flags.string({ + required: true, + description: 'RPC URL for the Ethereum L1 network', + }), + gaslimit: Flags.integer({ + description: 'Gas limit for the L2 transaction', + default: 100000, + }), + } + + requireSynced = false + + async init() { + // noop - skip ContractKit initialization, we use L1 directly + } + + async run() { + const res = await this.parse(BridgeDeposit) + const { from, value, network: networkFlag, l1RpcUrl, gaslimit } = res.flags + const to = res.flags.to || from + const network = validateNetwork(networkFlag) + const config = BRIDGE_CONFIG[network] + + // We need wallet client for L1 - derive from BaseCommand's signing config + const wallet = await this.getL1WalletClient(res, l1RpcUrl, network) + const l1Client = createPublicClient({ + chain: config.l1Chain, + transport: http(l1RpcUrl), + }) + + // Step 1: Retrieve gas paying token (CELO address on L1) + ux.action.start('Step 1/3: Retrieving CELO token address on L1') + const [celoL1Address] = await l1Client.readContract({ + address: config.systemConfig, + abi: SYSTEM_CONFIG_ABI, + functionName: 'gasPayingToken', + }) + ux.action.stop() + printValueMap({ 'CELO token on L1': celoL1Address }) + + // Step 2: Approve OptimismPortal to spend CELO + ux.action.start('Step 2/3: Approving CELO spending on L1') + const approveHash = await wallet.writeContract({ + address: celoL1Address, + abi: ERC20_APPROVE_ABI, + functionName: 'approve', + args: [config.optimismPortal, value], + chain: config.l1Chain, + account: wallet.account!, + } as any) + const approveReceipt = await l1Client.waitForTransactionReceipt({ hash: approveHash }) + ux.action.stop() + printValueMap({ 'Approval txHash': approveReceipt.transactionHash }) + + // Step 3: Deposit CELO to L2 + ux.action.start('Step 3/3: Depositing CELO to L2') + const depositHash = await wallet.writeContract({ + address: config.optimismPortal, + abi: OPTIMISM_PORTAL_DEPOSIT_ABI, + functionName: 'depositERC20Transaction', + args: [to, value, value, BigInt(gaslimit), false, '0x00'], + chain: config.l1Chain, + account: wallet.account!, + } as any) + const depositReceipt = await l1Client.waitForTransactionReceipt({ hash: depositHash }) + ux.action.stop() + printValueMap({ + 'Deposit txHash': depositReceipt.transactionHash, + status: depositReceipt.status === 'success' ? 'Success' : 'Failed', + }) + + console.log( + '\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.' + ) + } + + // Create an L1 wallet client using the same signing mechanism as BaseCommand + private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { + const config = BRIDGE_CONFIG[network] + + if (res.flags.useLedger) { + const ledgerOptions = await this.ledgerOptions() + const { ledgerToWalletClient } = await import('@celo/viem-account-ledger') + return ledgerToWalletClient({ + ...ledgerOptions, + account: res.flags.from, + walletClientOptions: { + transport: http(l1RpcUrl), + chain: config.l1Chain, + }, + }) + } else if (res.flags.privateKey) { + const { ensureLeading0x } = await import('@celo/base') + const account = privateKeyToAccount(ensureLeading0x(res.flags.privateKey)) + return createWalletClient({ + account, + chain: config.l1Chain, + transport: http(l1RpcUrl), + }) + } + + throw new Error( + 'Bridge commands require --privateKey or --useLedger for signing L1 transactions' + ) + } +} diff --git a/packages/cli/src/commands/bridge/withdraw-finalize.ts b/packages/cli/src/commands/bridge/withdraw-finalize.ts new file mode 100644 index 0000000000..e35c3dac84 --- /dev/null +++ b/packages/cli/src/commands/bridge/withdraw-finalize.ts @@ -0,0 +1,146 @@ +import { ensureLeading0x } from '@celo/base' +import { Flags, ux } from '@oclif/core' +import { createPublicClient, createWalletClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { getWithdrawals, publicActionsL2, walletActionsL1 } from 'viem/op-stack' +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' +import { CustomFlags } from '../../utils/command' +import { + BRIDGE_CONFIG, + validateNetwork, + getL2OpChain, + createL1PublicClient, + type BridgeNetwork, +} from '../../utils/bridge' + +export default class BridgeWithdrawFinalize extends BaseCommand { + static description = + 'Finalize a withdrawal and claim your CELO on Ethereum (L1). This is the final step of the withdrawal process.\n\nCan only be run after the 7-day challenge period has passed. Use bridge:withdraw-status to check if your withdrawal is ready.' + + static examples = [ + 'bridge:withdraw-finalize --txHash 0xYOUR_L2_TX_HASH --from 0xYOUR_ADDRESS --network mainnet --l1RpcUrl https://eth-mainnet.example.com -n mainnet -k 0xPRIVATE_KEY', + ] + + static flags = { + ...BaseCommand.flags, + txHash: CustomFlags.hexString({ + required: true, + description: 'Transaction hash of the withdrawal initiation on L2', + }), + from: CustomFlags.address({ + required: true, + description: 'Address that will finalize the withdrawal on L1 (pays L1 gas)', + }), + network: Flags.string({ + required: true, + options: ['mainnet', 'sepolia'], + description: 'Network (mainnet or sepolia)', + }), + l1RpcUrl: Flags.string({ + required: true, + description: 'RPC URL for the Ethereum L1 network', + }), + } + + requireSynced = false + + async init() { + // noop — skip ContractKit + } + + async run() { + const res = await this.parse(BridgeWithdrawFinalize) + const { txHash, network: networkFlag, l1RpcUrl } = res.flags + const network = validateNetwork(networkFlag) + const l2Chain = getL2OpChain(network) + + // Create L2 client with OP Stack extensions + const l2NodeUrl = await this.getNodeUrl() + const l2Client = createPublicClient({ + chain: l2Chain, + transport: http(l2NodeUrl), + }).extend(publicActionsL2()) + + // Create L1 clients + const l1Client = createL1PublicClient(l1RpcUrl, network) + const l1Wallet = await this.getL1WalletClient(res, l1RpcUrl, network) + + // Step 1: Get the withdrawal receipt + ux.action.start('Step 1/3: Fetching withdrawal transaction receipt') + const receipt = await l2Client.getTransactionReceipt({ hash: txHash as `0x${string}` }) + ux.action.stop() + + // Step 2: Check status + ux.action.start('Step 2/3: Verifying withdrawal is ready to finalize') + const status = await l1Client.getWithdrawalStatus({ + receipt, + targetChain: l2Chain as any, + }) + ux.action.stop() + + if (status !== 'ready-to-finalize') { + const statusMessages: Record = { + 'waiting-to-prove': + 'The withdrawal has not been proven yet. Run bridge:withdraw-prove first.', + 'ready-to-prove': + 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', + 'waiting-to-finalize': + 'The 7-day challenge period has not passed yet. Please wait and try again later.', + finalized: 'This withdrawal has already been finalized.', + } + const msg = statusMessages[status] || `Unexpected status: ${status}` + throw new Error(`Cannot finalize: ${msg}`) + } + + // Step 3: Finalize the withdrawal on L1 + ux.action.start('Step 3/3: Finalizing withdrawal on L1') + const [withdrawal] = getWithdrawals(receipt) + const finalizeHash = await l1Wallet.finalizeWithdrawal({ + targetChain: l2Chain as any, + withdrawal, + }) + + const finalizeReceipt = await createPublicClient({ + chain: BRIDGE_CONFIG[network].l1Chain, + transport: http(l1RpcUrl), + }).waitForTransactionReceipt({ hash: finalizeHash }) + ux.action.stop() + + printValueMap({ + 'Finalize txHash': finalizeReceipt.transactionHash, + status: finalizeReceipt.status === 'success' ? 'Success' : 'Failed', + }) + + console.log('\nWithdrawal finalized! Your CELO has been sent to your L1 address.') + } + + private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { + const config = BRIDGE_CONFIG[network] + + if (res.flags.useLedger) { + const ledgerOptions = await this.ledgerOptions() + const { ledgerToWalletClient } = await import('@celo/viem-account-ledger') + const wallet = await ledgerToWalletClient({ + ...ledgerOptions, + account: res.flags.from, + walletClientOptions: { + transport: http(l1RpcUrl), + chain: config.l1Chain, + }, + }) + return wallet.extend(walletActionsL1()) + } else if (res.flags.privateKey) { + const account = privateKeyToAccount(ensureLeading0x(res.flags.privateKey)) + return createWalletClient({ + account, + chain: config.l1Chain, + transport: http(l1RpcUrl), + }).extend(walletActionsL1()) + } + + throw new Error( + 'Bridge commands require --privateKey or --useLedger for signing L1 transactions' + ) + } +} diff --git a/packages/cli/src/commands/bridge/withdraw-init.ts b/packages/cli/src/commands/bridge/withdraw-init.ts new file mode 100644 index 0000000000..b8ca907985 --- /dev/null +++ b/packages/cli/src/commands/bridge/withdraw-init.ts @@ -0,0 +1,84 @@ +import { Flags } from '@oclif/core' +import { BaseCommand } from '../../base' +import { displayViemTx } from '../../utils/cli' +import { CustomFlags } from '../../utils/command' +import { newCheckBuilder } from '../../utils/checks' +import { + BRIDGE_CONFIG, + L2_L1_MESSAGE_PASSER_ABI, + validateNetwork, +} from '../../utils/bridge' + +export default class BridgeWithdrawInit extends BaseCommand { + static description = + 'Initiate a withdrawal of CELO from Celo (L2) to Ethereum (L1). This is step 1 of the withdrawal process.\n\nAfter initiating, you will need to:\n1. Wait ~1 hour for the proof to become available\n2. Run bridge:withdraw-prove to submit the proof\n3. Wait 7 days for the challenge period\n4. Run bridge:withdraw-finalize to claim your funds on L1' + + static examples = [ + 'bridge:withdraw-init --from 0xYOUR_L2_ADDRESS --to 0xL1_RECIPIENT --value 1000000000000000000 --network mainnet -n mainnet -k 0xPRIVATE_KEY', + 'bridge:withdraw-init --from 0xYOUR_L2_ADDRESS --value 1000000000000000000 --network sepolia -n celo-sepolia -k 0xPRIVATE_KEY', + ] + + static flags = { + ...BaseCommand.flags, + from: CustomFlags.address({ + required: true, + description: 'Address of the sender on L2', + }), + to: CustomFlags.address({ + description: 'Address of the recipient on L1 (defaults to sender address)', + }), + value: CustomFlags.bigint({ + required: true, + description: 'Amount of CELO to withdraw (in wei)', + }), + network: Flags.string({ + required: true, + options: ['mainnet', 'sepolia'], + description: 'Network to bridge on (mainnet or sepolia)', + }), + } + + async init() { + // noop — skip ContractKit, we use viem directly + } + + async run() { + const client = await this.getPublicClient() + const wallet = await this.getWalletClient() + const res = await this.parse(BridgeWithdrawInit) + + const { from, value, network: networkFlag } = res.flags + const to = res.flags.to || from + const network = validateNetwork(networkFlag) + const config = BRIDGE_CONFIG[network] + + await newCheckBuilder(this) + .isNotSanctioned(from) + .isNotSanctioned(to) + .isValidWalletSigner(from) + .hasEnoughCelo(from, value) + .runChecks() + + console.log('\nInitiating withdrawal from L2 to L1...') + console.log('This sends your CELO to the L2→L1 message bridge.\n') + + const txHash = wallet.writeContract({ + address: config.l2L1MessagePasser, + abi: L2_L1_MESSAGE_PASSER_ABI, + functionName: 'initiateWithdrawal', + args: [to, BigInt(0), '0x00'], + value: value, + chain: client.chain, + account: wallet.account!, + } as any) as Promise<`0x${string}`> + + await displayViemTx('InitiateWithdrawal', txHash, client) + + console.log('\nWithdrawal initiated! Save the transaction hash above.') + console.log('Next steps:') + console.log(' 1. Wait ~1 hour for the proof to become available') + console.log(' 2. Run: celocli bridge:withdraw-prove --txHash ...') + console.log(' 3. Wait 7 days for the challenge period to pass') + console.log(' 4. Run: celocli bridge:withdraw-finalize --txHash ...') + } +} diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts new file mode 100644 index 0000000000..eca4405244 --- /dev/null +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -0,0 +1,146 @@ +import { ensureLeading0x } from '@celo/base' +import { Flags, ux } from '@oclif/core' +import { createPublicClient, createWalletClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { publicActionsL2, walletActionsL1 } from 'viem/op-stack' +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' +import { CustomFlags } from '../../utils/command' +import { + BRIDGE_CONFIG, + validateNetwork, + getL2OpChain, + createL1PublicClient, + type BridgeNetwork, +} from '../../utils/bridge' + +export default class BridgeWithdrawProve extends BaseCommand { + static description = + 'Build a withdrawal proof and submit it to Ethereum (L1). This is step 2 of the withdrawal process.\n\nThis command will wait until the proof is available (~1 hour after withdrawal initiation), then automatically build and submit it.' + + static examples = [ + 'bridge:withdraw-prove --txHash 0xYOUR_L2_TX_HASH --from 0xYOUR_ADDRESS --network mainnet --l1RpcUrl https://eth-mainnet.example.com -n mainnet -k 0xPRIVATE_KEY', + ] + + static flags = { + ...BaseCommand.flags, + txHash: CustomFlags.hexString({ + required: true, + description: 'Transaction hash of the withdrawal initiation on L2', + }), + from: CustomFlags.address({ + required: true, + description: 'Address that will submit the proof on L1 (pays L1 gas)', + }), + network: Flags.string({ + required: true, + options: ['mainnet', 'sepolia'], + description: 'Network to bridge on (mainnet or sepolia)', + }), + l1RpcUrl: Flags.string({ + required: true, + description: 'RPC URL for the Ethereum L1 network', + }), + } + + requireSynced = false + + async init() { + // noop — skip ContractKit + } + + async run() { + const res = await this.parse(BridgeWithdrawProve) + const { txHash, network: networkFlag, l1RpcUrl } = res.flags + const network = validateNetwork(networkFlag) + const l2Chain = getL2OpChain(network) + + // Create L2 client with OP Stack extensions + const l2NodeUrl = await this.getNodeUrl() + const l2Client = createPublicClient({ + chain: l2Chain, + transport: http(l2NodeUrl), + }).extend(publicActionsL2()) + + // Create L1 public client with OP Stack extensions + const l1Client = createL1PublicClient(l1RpcUrl, network) + + // Create L1 wallet client for submitting the prove tx + const l1Wallet = await this.getL1WalletClient(res, l1RpcUrl, network) + + // Step 1: Get the withdrawal receipt + ux.action.start('Step 1/4: Fetching withdrawal transaction receipt') + const receipt = await l2Client.getTransactionReceipt({ hash: txHash as `0x${string}` }) + ux.action.stop() + printValueMap({ + blockNumber: receipt.blockNumber.toString(), + status: receipt.status, + }) + + // Step 2: Check/wait for proof readiness + ux.action.start('Step 2/4: Waiting for proof to become available (this may take up to 1 hour)') + const { output, withdrawal } = await l1Client.waitToProve({ + receipt, + targetChain: l2Chain as any, + }) + ux.action.stop() + console.log('Proof is ready!') + + // Step 3: Build the proof + ux.action.start('Step 3/4: Building withdrawal proof') + const proveArgs = await l2Client.buildProveWithdrawal({ + output, + withdrawal, + }) + ux.action.stop() + console.log('Proof built successfully.') + + // Step 4: Submit the prove transaction on L1 + ux.action.start('Step 4/4: Submitting proof to L1') + const proveHash = await l1Wallet.proveWithdrawal(proveArgs) + const proveReceipt = await createPublicClient({ + chain: BRIDGE_CONFIG[network].l1Chain, + transport: http(l1RpcUrl), + }).waitForTransactionReceipt({ hash: proveHash }) + ux.action.stop() + + printValueMap({ + 'Prove txHash': proveReceipt.transactionHash, + status: proveReceipt.status === 'success' ? 'Success' : 'Failed', + }) + + console.log('\nWithdrawal proof submitted! Next steps:') + console.log(' 1. Wait 7 days for the challenge period to pass') + console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') + console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') + } + + private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { + const config = BRIDGE_CONFIG[network] + + if (res.flags.useLedger) { + const ledgerOptions = await this.ledgerOptions() + const { ledgerToWalletClient } = await import('@celo/viem-account-ledger') + const wallet = await ledgerToWalletClient({ + ...ledgerOptions, + account: res.flags.from, + walletClientOptions: { + transport: http(l1RpcUrl), + chain: config.l1Chain, + }, + }) + return wallet.extend(walletActionsL1()) + } else if (res.flags.privateKey) { + const account = privateKeyToAccount(ensureLeading0x(res.flags.privateKey)) + return createWalletClient({ + account, + chain: config.l1Chain, + transport: http(l1RpcUrl), + }).extend(walletActionsL1()) + } + + throw new Error( + 'Bridge commands require --privateKey or --useLedger for signing L1 transactions' + ) + } +} diff --git a/packages/cli/src/commands/bridge/withdraw-status.ts b/packages/cli/src/commands/bridge/withdraw-status.ts new file mode 100644 index 0000000000..6b6e95abc4 --- /dev/null +++ b/packages/cli/src/commands/bridge/withdraw-status.ts @@ -0,0 +1,128 @@ +import { Flags, ux } from '@oclif/core' +import { createPublicClient, http } from 'viem' +import { publicActionsL2 } from 'viem/op-stack' +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' +import { CustomFlags } from '../../utils/command' +import { + WITHDRAWAL_STATUS_LABELS, + validateNetwork, + getL2OpChain, + createL1PublicClient, +} from '../../utils/bridge' + +export default class BridgeWithdrawStatus extends BaseCommand { + static description = + 'Check the status of a CELO withdrawal from Celo (L2) to Ethereum (L1).\n\nProvide the L2 transaction hash from the initial bridge:withdraw-init command to see where your withdrawal stands in the process.' + + static examples = [ + 'bridge:withdraw-status --txHash 0xYOUR_L2_TX_HASH --network mainnet --l1RpcUrl https://eth-mainnet.example.com -n mainnet', + 'bridge:withdraw-status --txHash 0xYOUR_L2_TX_HASH --network sepolia --l1RpcUrl https://eth-sepolia.example.com -n celo-sepolia', + ] + + static flags = { + ...BaseCommand.flags, + txHash: CustomFlags.hexString({ + required: true, + description: 'Transaction hash of the withdrawal initiation on L2', + }), + network: Flags.string({ + required: true, + options: ['mainnet', 'sepolia'], + description: 'Network (mainnet or sepolia)', + }), + l1RpcUrl: Flags.string({ + required: true, + description: 'RPC URL for the Ethereum L1 network', + }), + } + + requireSynced = false + + // Read-only command + isOnlyReadingWallet = true + + async init() { + // noop — skip ContractKit + } + + async run() { + const res = await this.parse(BridgeWithdrawStatus) + const { txHash, network: networkFlag, l1RpcUrl } = res.flags + const network = validateNetwork(networkFlag) + const l2Chain = getL2OpChain(network) + + // Create L2 client with OP Stack extensions + const l2NodeUrl = await this.getNodeUrl() + const l2Client = createPublicClient({ + chain: l2Chain, + transport: http(l2NodeUrl), + }).extend(publicActionsL2()) + + // Create L1 client with OP Stack extensions + const l1Client = createL1PublicClient(l1RpcUrl, network) + + // Get the receipt + ux.action.start('Fetching withdrawal transaction') + const receipt = await l2Client.getTransactionReceipt({ hash: txHash as `0x${string}` }) + ux.action.stop() + + if (receipt.status !== 'success') { + console.log('\nThe withdrawal transaction failed on L2.') + printValueMap({ status: 'Failed', blockNumber: receipt.blockNumber.toString() }) + return + } + + // Get the withdrawal status from L1 + ux.action.start('Checking withdrawal status on L1') + const status = await l1Client.getWithdrawalStatus({ + receipt, + targetChain: l2Chain as any, + }) + ux.action.stop() + + const statusInfo = WITHDRAWAL_STATUS_LABELS[status as keyof typeof WITHDRAWAL_STATUS_LABELS] || { + label: status, + description: 'Unknown status', + } + + // Display human-readable status + console.log('') + console.log(` Status: ${statusInfo.label}`) + console.log('') + console.log(` ${statusInfo.description}`) + console.log('') + + // Show contextual next steps + printValueMap({ + 'L2 Transaction': txHash, + 'L2 Block': receipt.blockNumber.toString(), + 'Current Status': statusInfo.label, + }) + + console.log('') + switch (status) { + case 'waiting-to-prove': + console.log('What to do next:') + console.log(' Wait for the proof to become available (~1 hour after initiation).') + console.log(' Then run: celocli bridge:withdraw-prove --txHash ' + txHash + ' ...') + break + case 'ready-to-prove': + console.log('What to do next:') + console.log(' Run: celocli bridge:withdraw-prove --txHash ' + txHash + ' ...') + break + case 'waiting-to-finalize': + console.log('What to do next:') + console.log(' Wait for the 7-day challenge period to pass.') + console.log(' You can check again later with this same command.') + break + case 'ready-to-finalize': + console.log('What to do next:') + console.log(' Run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') + break + case 'finalized': + console.log('Your withdrawal is complete! Funds have been sent to L1.') + break + } + } +} diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts new file mode 100644 index 0000000000..c4f943dc54 --- /dev/null +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -0,0 +1,235 @@ +import BridgeWithdrawInit from './withdraw-init' +import BridgeWithdrawProve from './withdraw-prove' +import BridgeWithdrawStatus from './withdraw-status' +import BridgeWithdrawFinalize from './withdraw-finalize' + +// Inline testLocally to avoid importing cliUtils which pulls in @celo/dev-utils +async function testLocally(command: any, argv: string[]) { + if (argv.includes('--node')) { + return command.run(argv) + } + const extendedArgv = command.flags?.node ? [...argv, '--node', 'local'] : argv + return command.run(extendedArgv) +} + +process.env.NO_SYNCCHECK = 'true' + +jest.setTimeout(15000) + +describe('bridge:withdraw-init', () => { + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}) + jest.spyOn(console, 'error').mockImplementation(() => {}) + jest.spyOn(console, 'info').mockImplementation(() => {}) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('requires --from flag', async () => { + await expect( + testLocally(BridgeWithdrawInit, [ + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', + ]) + ).rejects.toThrow() + }) + + it('requires --value flag', async () => { + await expect( + testLocally(BridgeWithdrawInit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', + ]) + ).rejects.toThrow() + }) + + it('requires --network flag', async () => { + await expect( + testLocally(BridgeWithdrawInit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--node', 'celo-sepolia', + ]) + ).rejects.toThrow() + }) + + it('rejects invalid network', async () => { + await expect( + testLocally(BridgeWithdrawInit, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('rejects invalid from address', async () => { + await expect( + testLocally(BridgeWithdrawInit, [ + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', + ]) + ).rejects.toThrow('is not a valid address') + }) +}) + +describe('bridge:withdraw-prove', () => { + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}) + jest.spyOn(console, 'error').mockImplementation(() => {}) + jest.spyOn(console, 'info').mockImplementation(() => {}) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('requires --txHash flag', async () => { + await expect( + testLocally(BridgeWithdrawProve, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('requires --from flag', async () => { + await expect( + testLocally(BridgeWithdrawProve, [ + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('requires --l1RpcUrl flag', async () => { + await expect( + testLocally(BridgeWithdrawProve, [ + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('rejects invalid txHash format', async () => { + await expect( + testLocally(BridgeWithdrawProve, [ + '--txHash', 'not-a-hash', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) +}) + +describe('bridge:withdraw-status', () => { + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}) + jest.spyOn(console, 'error').mockImplementation(() => {}) + jest.spyOn(console, 'info').mockImplementation(() => {}) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('requires --txHash flag', async () => { + await expect( + testLocally(BridgeWithdrawStatus, [ + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + ]) + ).rejects.toThrow() + }) + + it('requires --l1RpcUrl flag', async () => { + await expect( + testLocally(BridgeWithdrawStatus, [ + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--node', 'celo-sepolia', + ]) + ).rejects.toThrow() + }) + + it('rejects invalid network', async () => { + await expect( + testLocally(BridgeWithdrawStatus, [ + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + ]) + ).rejects.toThrow() + }) +}) + +describe('bridge:withdraw-finalize', () => { + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}) + jest.spyOn(console, 'error').mockImplementation(() => {}) + jest.spyOn(console, 'info').mockImplementation(() => {}) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('requires --txHash flag', async () => { + await expect( + testLocally(BridgeWithdrawFinalize, [ + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('requires --from flag', async () => { + await expect( + testLocally(BridgeWithdrawFinalize, [ + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) + + it('rejects invalid network', async () => { + await expect( + testLocally(BridgeWithdrawFinalize, [ + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), + ]) + ).rejects.toThrow() + }) +}) diff --git a/packages/cli/src/utils/bridge.test.ts b/packages/cli/src/utils/bridge.test.ts new file mode 100644 index 0000000000..d6fc8c4834 --- /dev/null +++ b/packages/cli/src/utils/bridge.test.ts @@ -0,0 +1,68 @@ +import { validateNetwork, BRIDGE_CONFIG, WITHDRAWAL_STATUS_LABELS, getL2OpChain } from './bridge' + +describe('bridge utils', () => { + describe('validateNetwork', () => { + it('accepts mainnet', () => { + expect(validateNetwork('mainnet')).toBe('mainnet') + }) + + it('accepts sepolia', () => { + expect(validateNetwork('sepolia')).toBe('sepolia') + }) + + it('rejects invalid network', () => { + expect(() => validateNetwork('goerli')).toThrow('Invalid network: goerli') + }) + + it('rejects empty string', () => { + expect(() => validateNetwork('')).toThrow('Invalid network') + }) + }) + + describe('BRIDGE_CONFIG', () => { + it('has mainnet config with correct addresses', () => { + const config = BRIDGE_CONFIG.mainnet + expect(config.systemConfig).toBe('0x89E31965D844a309231B1f17759Ccaf1b7c09861') + expect(config.optimismPortal).toBe('0xc5c5D157928BDBD2ACf6d0777626b6C75a9EAEDC') + expect(config.l2L1MessagePasser).toBe('0x4200000000000000000000000000000000000016') + }) + + it('has sepolia config with correct addresses', () => { + const config = BRIDGE_CONFIG.sepolia + expect(config.systemConfig).toBe('0x760a5f022c9940f4a074e0030be682f560d29818') + expect(config.optimismPortal).toBe('0x44ae3d41a335a7d05eb533029917aad35662dcc2') + expect(config.l2L1MessagePasser).toBe('0x4200000000000000000000000000000000000016') + }) + }) + + describe('getL2OpChain', () => { + it('returns Celo mainnet chain for mainnet', () => { + const chain = getL2OpChain('mainnet') + expect(chain.id).toBe(42220) + expect(chain.name).toBe('Celo') + }) + + it('returns Celo Sepolia chain for sepolia', () => { + const chain = getL2OpChain('sepolia') + expect(chain.id).toBe(11142220) + expect(chain.name).toBe('Celo Sepolia Testnet') + }) + }) + + describe('WITHDRAWAL_STATUS_LABELS', () => { + it('has labels for all statuses', () => { + expect(WITHDRAWAL_STATUS_LABELS['waiting-to-prove']).toBeDefined() + expect(WITHDRAWAL_STATUS_LABELS['ready-to-prove']).toBeDefined() + expect(WITHDRAWAL_STATUS_LABELS['waiting-to-finalize']).toBeDefined() + expect(WITHDRAWAL_STATUS_LABELS['ready-to-finalize']).toBeDefined() + expect(WITHDRAWAL_STATUS_LABELS['finalized']).toBeDefined() + }) + + it('each status has label and description', () => { + Object.values(WITHDRAWAL_STATUS_LABELS).forEach((status) => { + expect(status.label).toBeTruthy() + expect(status.description).toBeTruthy() + }) + }) + }) +}) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts new file mode 100644 index 0000000000..aa0be15852 --- /dev/null +++ b/packages/cli/src/utils/bridge.ts @@ -0,0 +1,216 @@ +import { StrongAddress } from '@celo/base' +import { createPublicClient, http, type Chain } from 'viem' +import { mainnet, sepolia } from 'viem/chains' +import { chainConfig, publicActionsL1 } from 'viem/op-stack' +import { defineChain } from 'viem/utils' + +export type BridgeNetwork = 'mainnet' | 'sepolia' + +export interface BridgeConfig { + l1Chain: Chain + systemConfig: StrongAddress + optimismPortal: StrongAddress + l2L1MessagePasser: StrongAddress +} + +// Contract addresses per network — sourced from op-tooling deposit.sh / withdrawal scripts +export const BRIDGE_CONFIG: Record = { + mainnet: { + l1Chain: mainnet, + systemConfig: '0x89E31965D844a309231B1f17759Ccaf1b7c09861', + optimismPortal: '0xc5c5D157928BDBD2ACf6d0777626b6C75a9EAEDC', + l2L1MessagePasser: '0x4200000000000000000000000000000000000016', + }, + sepolia: { + l1Chain: sepolia, + systemConfig: '0x760a5f022c9940f4a074e0030be682f560d29818', + optimismPortal: '0x44ae3d41a335a7d05eb533029917aad35662dcc2', + l2L1MessagePasser: '0x4200000000000000000000000000000000000016', + }, +} + +// Celo Mainnet OP Stack chain definition for viem +// Standard viem `celo` chain lacks OP Stack contract addresses needed for +// withdrawal proving/finalization. This definition adds them. +const celoL2 = /*#__PURE__*/ defineChain({ + ...chainConfig, + id: 42_220, + name: 'Celo', + nativeCurrency: { decimals: 18, name: 'CELO', symbol: 'CELO' }, + rpcUrls: { default: { http: ['https://forno.celo.org'] } }, + blockExplorers: { + default: { + name: 'Celo Explorer', + url: 'https://celoscan.io', + apiUrl: 'https://api.celoscan.io/api', + }, + }, + contracts: { + ...chainConfig.contracts, + multicall3: { + address: '0xcA11bde05977b3631167028862bE2a173976CA11', + blockCreated: 13112599, + }, + portal: { + [1]: { address: '0xc5c5D157928BDBD2ACf6d0777626b6C75a9EAEDC' }, + }, + disputeGameFactory: { + [1]: { address: '0xFbAC162162f4009Bb007C6DeBC36B1dAC10aF683' }, + }, + l1StandardBridge: { + [1]: { address: '0x9C4955b92F34148dbcfDCD82e9c9eCe5CF2badfe' }, + }, + }, + sourceId: 1, + testnet: false, +}) + +// Celo Sepolia OP Stack chain definition for viem +const celoSepoliaL2 = /*#__PURE__*/ defineChain({ + ...chainConfig, + id: 11_142_220, + name: 'Celo Sepolia Testnet', + nativeCurrency: { decimals: 18, name: 'CELO', symbol: 'S-CELO' }, + rpcUrls: { default: { http: ['https://forno.celo-sepolia.celo-testnet.org'] } }, + blockExplorers: { + default: { + name: 'Celo Sepolia Explorer', + url: 'https://celo-sepolia.blockscout.com', + apiUrl: 'https://celo-sepolia.blockscout.com/api', + }, + }, + contracts: { + ...chainConfig.contracts, + multicall3: { + address: '0xcA11bde05977b3631167028862bE2a173976CA11', + blockCreated: 1, + }, + portal: { + [11155111]: { address: '0x44ae3d41a335a7d05eb533029917aad35662dcc2', blockCreated: 8825790 }, + }, + disputeGameFactory: { + [11155111]: { address: '0x57c45d82d1a995f1e135b8d7edc0a6bb5211cfaa', blockCreated: 8825790 }, + }, + l1StandardBridge: { + [11155111]: { address: '0xec18a3c30131a0db4246e785355fbc16e2eaf408', blockCreated: 8825790 }, + }, + }, + sourceId: 11155111, + testnet: true, +}) + +export function validateNetwork(network: string): BridgeNetwork { + if (network === 'mainnet' || network === 'sepolia') { + return network + } + throw new Error(`Invalid network: ${network}. Must be 'mainnet' or 'sepolia'.`) +} + +export function getL2OpChain(network: BridgeNetwork) { + return network === 'mainnet' ? celoL2 : celoSepoliaL2 +} + +export function createL1PublicClient(l1RpcUrl: string, network: BridgeNetwork): any { + const config = BRIDGE_CONFIG[network] + return createPublicClient({ + chain: config.l1Chain, + transport: http(l1RpcUrl), + }).extend(publicActionsL1()) +} + +// ABI fragments + +export const SYSTEM_CONFIG_ABI = [ + { + name: 'gasPayingToken', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [ + { name: 'addr_', type: 'address' }, + { name: 'decimals_', type: 'uint8' }, + ], + }, +] as const + +export const ERC20_APPROVE_ABI = [ + { + name: 'approve', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + }, +] as const + +export const OPTIMISM_PORTAL_DEPOSIT_ABI = [ + { + name: 'depositERC20Transaction', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: '_to', type: 'address' }, + { name: '_mint', type: 'uint256' }, + { name: '_value', type: 'uint256' }, + { name: '_gasLimit', type: 'uint64' }, + { name: '_isCreation', type: 'bool' }, + { name: '_data', type: 'bytes' }, + ], + outputs: [], + }, +] as const + +export const L2_L1_MESSAGE_PASSER_ABI = [ + { + name: 'initiateWithdrawal', + type: 'function', + stateMutability: 'payable', + inputs: [ + { name: '_target', type: 'address' }, + { name: '_gasLimit', type: 'uint256' }, + { name: '_data', type: 'bytes' }, + ], + outputs: [], + }, +] as const + +// Human-readable withdrawal status labels +export type WithdrawalStatus = + | 'waiting-to-prove' + | 'ready-to-prove' + | 'waiting-to-finalize' + | 'ready-to-finalize' + | 'finalized' + +export const WITHDRAWAL_STATUS_LABELS: Record< + WithdrawalStatus, + { label: string; description: string } +> = { + 'waiting-to-prove': { + label: 'Waiting to Prove', + description: + 'Your withdrawal has been initiated on L2. Wait ~1 hour for the proof to become available.', + }, + 'ready-to-prove': { + label: 'Ready to Prove', + description: + 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + }, + 'waiting-to-finalize': { + label: 'Waiting to Finalize', + description: + 'The proof has been submitted. The 7-day challenge period is in progress.', + }, + 'ready-to-finalize': { + label: 'Ready to Finalize', + description: + 'The challenge period has passed. Claim your funds on L1 with bridge:withdraw-finalize.', + }, + finalized: { + label: 'Finalized', + description: 'The withdrawal is complete. Funds have been sent to your L1 address.', + }, +} From 9b651268c44529290c616a00728c0a41ba41b58c Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 14:36:26 +0200 Subject: [PATCH 02/15] Formatting. --- .../cli/src/commands/bridge/deposit.test.ts | 90 +++++--- packages/cli/src/commands/bridge/deposit.ts | 4 +- .../src/commands/bridge/withdraw-finalize.ts | 3 +- .../cli/src/commands/bridge/withdraw-init.ts | 6 +- .../cli/src/commands/bridge/withdraw-prove.ts | 4 +- .../src/commands/bridge/withdraw-status.ts | 4 +- .../cli/src/commands/bridge/withdraw.test.ts | 195 ++++++++++++------ packages/cli/src/utils/bridge.ts | 6 +- 8 files changed, 201 insertions(+), 111 deletions(-) diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts index 07421d828b..051fd7cf8f 100644 --- a/packages/cli/src/commands/bridge/deposit.test.ts +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -27,10 +27,14 @@ describe('bridge:deposit', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -38,10 +42,14 @@ describe('bridge:deposit', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -49,10 +57,14 @@ describe('bridge:deposit', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -60,10 +72,14 @@ describe('bridge:deposit', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -71,11 +87,16 @@ describe('bridge:deposit', () => { it('rejects invalid network value', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -83,11 +104,16 @@ describe('bridge:deposit', () => { it('rejects invalid address format', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow('is not a valid address') }) @@ -95,10 +121,14 @@ describe('bridge:deposit', () => { it('requires signing method (privateKey or useLedger)', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/commands/bridge/deposit.ts b/packages/cli/src/commands/bridge/deposit.ts index 35cbcbec82..54148dba7b 100644 --- a/packages/cli/src/commands/bridge/deposit.ts +++ b/packages/cli/src/commands/bridge/deposit.ts @@ -111,9 +111,7 @@ export default class BridgeDeposit extends BaseCommand { status: depositReceipt.status === 'success' ? 'Success' : 'Failed', }) - console.log( - '\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.' - ) + console.log('\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.') } // Create an L1 wallet client using the same signing mechanism as BaseCommand diff --git a/packages/cli/src/commands/bridge/withdraw-finalize.ts b/packages/cli/src/commands/bridge/withdraw-finalize.ts index e35c3dac84..4766e7f437 100644 --- a/packages/cli/src/commands/bridge/withdraw-finalize.ts +++ b/packages/cli/src/commands/bridge/withdraw-finalize.ts @@ -83,8 +83,7 @@ export default class BridgeWithdrawFinalize extends BaseCommand { const statusMessages: Record = { 'waiting-to-prove': 'The withdrawal has not been proven yet. Run bridge:withdraw-prove first.', - 'ready-to-prove': - 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', + 'ready-to-prove': 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', 'waiting-to-finalize': 'The 7-day challenge period has not passed yet. Please wait and try again later.', finalized: 'This withdrawal has already been finalized.', diff --git a/packages/cli/src/commands/bridge/withdraw-init.ts b/packages/cli/src/commands/bridge/withdraw-init.ts index b8ca907985..c9a1a45c70 100644 --- a/packages/cli/src/commands/bridge/withdraw-init.ts +++ b/packages/cli/src/commands/bridge/withdraw-init.ts @@ -3,11 +3,7 @@ import { BaseCommand } from '../../base' import { displayViemTx } from '../../utils/cli' import { CustomFlags } from '../../utils/command' import { newCheckBuilder } from '../../utils/checks' -import { - BRIDGE_CONFIG, - L2_L1_MESSAGE_PASSER_ABI, - validateNetwork, -} from '../../utils/bridge' +import { BRIDGE_CONFIG, L2_L1_MESSAGE_PASSER_ABI, validateNetwork } from '../../utils/bridge' export default class BridgeWithdrawInit extends BaseCommand { static description = diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts index eca4405244..fbe20808b0 100644 --- a/packages/cli/src/commands/bridge/withdraw-prove.ts +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -112,7 +112,9 @@ export default class BridgeWithdrawProve extends BaseCommand { console.log('\nWithdrawal proof submitted! Next steps:') console.log(' 1. Wait 7 days for the challenge period to pass') console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') - console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') + console.log( + ' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...' + ) } private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { diff --git a/packages/cli/src/commands/bridge/withdraw-status.ts b/packages/cli/src/commands/bridge/withdraw-status.ts index 6b6e95abc4..3e82f5435e 100644 --- a/packages/cli/src/commands/bridge/withdraw-status.ts +++ b/packages/cli/src/commands/bridge/withdraw-status.ts @@ -81,7 +81,9 @@ export default class BridgeWithdrawStatus extends BaseCommand { }) ux.action.stop() - const statusInfo = WITHDRAWAL_STATUS_LABELS[status as keyof typeof WITHDRAWAL_STATUS_LABELS] || { + const statusInfo = WITHDRAWAL_STATUS_LABELS[ + status as keyof typeof WITHDRAWAL_STATUS_LABELS + ] || { label: status, description: 'Unknown status', } diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts index c4f943dc54..133b50bbea 100644 --- a/packages/cli/src/commands/bridge/withdraw.test.ts +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -30,9 +30,12 @@ describe('bridge:withdraw-init', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -40,9 +43,12 @@ describe('bridge:withdraw-init', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -50,9 +56,12 @@ describe('bridge:withdraw-init', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -60,11 +69,16 @@ describe('bridge:withdraw-init', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -72,10 +86,14 @@ describe('bridge:withdraw-init', () => { it('rejects invalid from address', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow('is not a valid address') }) @@ -95,11 +113,16 @@ describe('bridge:withdraw-prove', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -107,11 +130,16 @@ describe('bridge:withdraw-prove', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -119,11 +147,16 @@ describe('bridge:withdraw-prove', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -131,12 +164,18 @@ describe('bridge:withdraw-prove', () => { it('rejects invalid txHash format', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', 'not-a-hash', - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + 'not-a-hash', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -156,9 +195,12 @@ describe('bridge:withdraw-status', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -166,9 +208,12 @@ describe('bridge:withdraw-status', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -176,10 +221,14 @@ describe('bridge:withdraw-status', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -199,11 +248,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -211,11 +265,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -223,12 +282,18 @@ describe('bridge:withdraw-finalize', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index aa0be15852..652812d95f 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -196,13 +196,11 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: - 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: - 'The proof has been submitted. The 7-day challenge period is in progress.', + description: 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From 59211d4e1b49a407688ab847bbcedf28480a0b07 Mon Sep 17 00:00:00 2001 From: "Mc01.eth" Date: Mon, 20 Apr 2026 14:39:09 +0200 Subject: [PATCH 03/15] Update packages/cli/docs/bridge.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/cli/docs/bridge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/docs/bridge.md b/packages/cli/docs/bridge.md index b7a894c012..aee906483a 100644 --- a/packages/cli/docs/bridge.md +++ b/packages/cli/docs/bridge.md @@ -144,7 +144,7 @@ celocli bridge:withdraw-status \ | Network | L1 | L2 | |---------|----|----| | `mainnet` | Ethereum Mainnet | Celo Mainnet | -| `sepolia` | Ethereum Sepolia | Celo Alfajores | +| `sepolia` | Ethereum Sepolia | Celo Sepolia | ## Using with Ledger From 8bddf04a057abd716bb84aef19ec5071b4a5e40b Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 15:05:35 +0200 Subject: [PATCH 04/15] Copilot CR. --- .../cli/src/commands/bridge/deposit.test.ts | 90 +++----- packages/cli/src/commands/bridge/deposit.ts | 23 ++- .../src/commands/bridge/withdraw-finalize.ts | 10 +- .../cli/src/commands/bridge/withdraw-init.ts | 9 +- .../cli/src/commands/bridge/withdraw-prove.ts | 11 +- .../src/commands/bridge/withdraw-status.ts | 4 +- .../cli/src/commands/bridge/withdraw.test.ts | 195 ++++++------------ packages/cli/src/utils/bridge.ts | 8 +- 8 files changed, 141 insertions(+), 209 deletions(-) diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts index 051fd7cf8f..07421d828b 100644 --- a/packages/cli/src/commands/bridge/deposit.test.ts +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -27,14 +27,10 @@ describe('bridge:deposit', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -42,14 +38,10 @@ describe('bridge:deposit', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -57,14 +49,10 @@ describe('bridge:deposit', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -72,14 +60,10 @@ describe('bridge:deposit', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -87,16 +71,11 @@ describe('bridge:deposit', () => { it('rejects invalid network value', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -104,16 +83,11 @@ describe('bridge:deposit', () => { it('rejects invalid address format', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - 'not-an-address', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow('is not a valid address') }) @@ -121,14 +95,10 @@ describe('bridge:deposit', () => { it('requires signing method (privateKey or useLedger)', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/commands/bridge/deposit.ts b/packages/cli/src/commands/bridge/deposit.ts index 54148dba7b..552b7dbf23 100644 --- a/packages/cli/src/commands/bridge/deposit.ts +++ b/packages/cli/src/commands/bridge/deposit.ts @@ -1,5 +1,5 @@ import { Flags, ux } from '@oclif/core' -import { createPublicClient, createWalletClient, http } from 'viem' +import { createPublicClient, createWalletClient, http, isAddressEqual } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { BaseCommand } from '../../base' import { printValueMap } from '../../utils/cli' @@ -44,7 +44,7 @@ export default class BridgeDeposit extends BaseCommand { required: true, description: 'RPC URL for the Ethereum L1 network', }), - gaslimit: Flags.integer({ + gasLimit: Flags.integer({ description: 'Gas limit for the L2 transaction', default: 100000, }), @@ -58,7 +58,7 @@ export default class BridgeDeposit extends BaseCommand { async run() { const res = await this.parse(BridgeDeposit) - const { from, value, network: networkFlag, l1RpcUrl, gaslimit } = res.flags + const { from, value, network: networkFlag, l1RpcUrl, gasLimit } = res.flags const to = res.flags.to || from const network = validateNetwork(networkFlag) const config = BRIDGE_CONFIG[network] @@ -82,6 +82,7 @@ export default class BridgeDeposit extends BaseCommand { // Step 2: Approve OptimismPortal to spend CELO ux.action.start('Step 2/3: Approving CELO spending on L1') + // Type assertion needed: wallet client is created dynamically so TS can't infer chain/account const approveHash = await wallet.writeContract({ address: celoL1Address, abi: ERC20_APPROVE_ABI, @@ -100,7 +101,8 @@ export default class BridgeDeposit extends BaseCommand { address: config.optimismPortal, abi: OPTIMISM_PORTAL_DEPOSIT_ABI, functionName: 'depositERC20Transaction', - args: [to, value, value, BigInt(gaslimit), false, '0x00'], + // Type assertion needed: wallet client is created dynamically so TS can't infer chain/account + args: [to, value, value, BigInt(gasLimit), false, '0x'], chain: config.l1Chain, account: wallet.account!, } as any) @@ -111,7 +113,13 @@ export default class BridgeDeposit extends BaseCommand { status: depositReceipt.status === 'success' ? 'Success' : 'Failed', }) - console.log('\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.') + if (depositReceipt.status === 'success') { + console.log( + '\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.' + ) + } else { + throw new Error('Deposit transaction failed. Please check the transaction on a block explorer.') + } } // Create an L1 wallet client using the same signing mechanism as BaseCommand @@ -132,6 +140,11 @@ export default class BridgeDeposit extends BaseCommand { } else if (res.flags.privateKey) { const { ensureLeading0x } = await import('@celo/base') const account = privateKeyToAccount(ensureLeading0x(res.flags.privateKey)) + if (res.flags.from && !isAddressEqual(res.flags.from, account.address)) { + throw new Error( + `The --from address ${res.flags.from} does not match the address derived from the provided private key ${account.address}.` + ) + } return createWalletClient({ account, chain: config.l1Chain, diff --git a/packages/cli/src/commands/bridge/withdraw-finalize.ts b/packages/cli/src/commands/bridge/withdraw-finalize.ts index 4766e7f437..bf994c058c 100644 --- a/packages/cli/src/commands/bridge/withdraw-finalize.ts +++ b/packages/cli/src/commands/bridge/withdraw-finalize.ts @@ -1,6 +1,6 @@ import { ensureLeading0x } from '@celo/base' import { Flags, ux } from '@oclif/core' -import { createPublicClient, createWalletClient, http } from 'viem' +import { createPublicClient, createWalletClient, http, isAddressEqual } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { getWithdrawals, publicActionsL2, walletActionsL1 } from 'viem/op-stack' import { BaseCommand } from '../../base' @@ -83,7 +83,8 @@ export default class BridgeWithdrawFinalize extends BaseCommand { const statusMessages: Record = { 'waiting-to-prove': 'The withdrawal has not been proven yet. Run bridge:withdraw-prove first.', - 'ready-to-prove': 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', + 'ready-to-prove': + 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', 'waiting-to-finalize': 'The 7-day challenge period has not passed yet. Please wait and try again later.', finalized: 'This withdrawal has already been finalized.', @@ -131,6 +132,11 @@ export default class BridgeWithdrawFinalize extends BaseCommand { return wallet.extend(walletActionsL1()) } else if (res.flags.privateKey) { const account = privateKeyToAccount(ensureLeading0x(res.flags.privateKey)) + if (res.flags.from && !isAddressEqual(res.flags.from, account.address)) { + throw new Error( + `The --from address ${res.flags.from} does not match the address derived from the provided private key ${account.address}.` + ) + } return createWalletClient({ account, chain: config.l1Chain, diff --git a/packages/cli/src/commands/bridge/withdraw-init.ts b/packages/cli/src/commands/bridge/withdraw-init.ts index c9a1a45c70..26d8117504 100644 --- a/packages/cli/src/commands/bridge/withdraw-init.ts +++ b/packages/cli/src/commands/bridge/withdraw-init.ts @@ -3,7 +3,11 @@ import { BaseCommand } from '../../base' import { displayViemTx } from '../../utils/cli' import { CustomFlags } from '../../utils/command' import { newCheckBuilder } from '../../utils/checks' -import { BRIDGE_CONFIG, L2_L1_MESSAGE_PASSER_ABI, validateNetwork } from '../../utils/bridge' +import { + BRIDGE_CONFIG, + L2_L1_MESSAGE_PASSER_ABI, + validateNetwork, +} from '../../utils/bridge' export default class BridgeWithdrawInit extends BaseCommand { static description = @@ -62,7 +66,8 @@ export default class BridgeWithdrawInit extends BaseCommand { address: config.l2L1MessagePasser, abi: L2_L1_MESSAGE_PASSER_ABI, functionName: 'initiateWithdrawal', - args: [to, BigInt(0), '0x00'], + // Type assertion needed: wallet client is created dynamically so TS can't infer chain/account + args: [to, BigInt(0), '0x'], value: value, chain: client.chain, account: wallet.account!, diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts index fbe20808b0..47b6545487 100644 --- a/packages/cli/src/commands/bridge/withdraw-prove.ts +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -1,6 +1,6 @@ import { ensureLeading0x } from '@celo/base' import { Flags, ux } from '@oclif/core' -import { createPublicClient, createWalletClient, http } from 'viem' +import { createPublicClient, createWalletClient, http, isAddressEqual } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { publicActionsL2, walletActionsL1 } from 'viem/op-stack' import { BaseCommand } from '../../base' @@ -112,9 +112,7 @@ export default class BridgeWithdrawProve extends BaseCommand { console.log('\nWithdrawal proof submitted! Next steps:') console.log(' 1. Wait 7 days for the challenge period to pass') console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') - console.log( - ' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...' - ) + console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') } private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { @@ -134,6 +132,11 @@ export default class BridgeWithdrawProve extends BaseCommand { return wallet.extend(walletActionsL1()) } else if (res.flags.privateKey) { const account = privateKeyToAccount(ensureLeading0x(res.flags.privateKey)) + if (res.flags.from && !isAddressEqual(res.flags.from, account.address)) { + throw new Error( + `The --from address ${res.flags.from} does not match the address derived from the provided private key ${account.address}.` + ) + } return createWalletClient({ account, chain: config.l1Chain, diff --git a/packages/cli/src/commands/bridge/withdraw-status.ts b/packages/cli/src/commands/bridge/withdraw-status.ts index 3e82f5435e..6b6e95abc4 100644 --- a/packages/cli/src/commands/bridge/withdraw-status.ts +++ b/packages/cli/src/commands/bridge/withdraw-status.ts @@ -81,9 +81,7 @@ export default class BridgeWithdrawStatus extends BaseCommand { }) ux.action.stop() - const statusInfo = WITHDRAWAL_STATUS_LABELS[ - status as keyof typeof WITHDRAWAL_STATUS_LABELS - ] || { + const statusInfo = WITHDRAWAL_STATUS_LABELS[status as keyof typeof WITHDRAWAL_STATUS_LABELS] || { label: status, description: 'Unknown status', } diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts index 133b50bbea..c4f943dc54 100644 --- a/packages/cli/src/commands/bridge/withdraw.test.ts +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -30,12 +30,9 @@ describe('bridge:withdraw-init', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -43,12 +40,9 @@ describe('bridge:withdraw-init', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -56,12 +50,9 @@ describe('bridge:withdraw-init', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--node', - 'celo-sepolia', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -69,16 +60,11 @@ describe('bridge:withdraw-init', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'goerli', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -86,14 +72,10 @@ describe('bridge:withdraw-init', () => { it('rejects invalid from address', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - 'not-an-address', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow('is not a valid address') }) @@ -113,16 +95,11 @@ describe('bridge:withdraw-prove', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -130,16 +107,11 @@ describe('bridge:withdraw-prove', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -147,16 +119,11 @@ describe('bridge:withdraw-prove', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -164,18 +131,12 @@ describe('bridge:withdraw-prove', () => { it('rejects invalid txHash format', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - 'not-a-hash', - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', 'not-a-hash', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -195,12 +156,9 @@ describe('bridge:withdraw-status', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -208,12 +166,9 @@ describe('bridge:withdraw-status', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -221,14 +176,10 @@ describe('bridge:withdraw-status', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -248,16 +199,11 @@ describe('bridge:withdraw-finalize', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -265,16 +211,11 @@ describe('bridge:withdraw-finalize', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -282,18 +223,12 @@ describe('bridge:withdraw-finalize', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index 652812d95f..003f09b170 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -110,7 +110,7 @@ export function getL2OpChain(network: BridgeNetwork) { return network === 'mainnet' ? celoL2 : celoSepoliaL2 } -export function createL1PublicClient(l1RpcUrl: string, network: BridgeNetwork): any { +export function createL1PublicClient(l1RpcUrl: string, network: BridgeNetwork) { const config = BRIDGE_CONFIG[network] return createPublicClient({ chain: config.l1Chain, @@ -196,11 +196,13 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: + 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: 'The proof has been submitted. The 7-day challenge period is in progress.', + description: + 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From bbd643bcde8ec56a3defe51bf0e8ab0e3fa064c6 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 15:05:57 +0200 Subject: [PATCH 05/15] Formatting. --- .../cli/src/commands/bridge/deposit.test.ts | 90 +++++--- packages/cli/src/commands/bridge/deposit.ts | 8 +- .../src/commands/bridge/withdraw-finalize.ts | 3 +- .../cli/src/commands/bridge/withdraw-init.ts | 6 +- .../cli/src/commands/bridge/withdraw-prove.ts | 4 +- .../src/commands/bridge/withdraw-status.ts | 4 +- .../cli/src/commands/bridge/withdraw.test.ts | 195 ++++++++++++------ packages/cli/src/utils/bridge.ts | 6 +- 8 files changed, 204 insertions(+), 112 deletions(-) diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts index 07421d828b..051fd7cf8f 100644 --- a/packages/cli/src/commands/bridge/deposit.test.ts +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -27,10 +27,14 @@ describe('bridge:deposit', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -38,10 +42,14 @@ describe('bridge:deposit', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -49,10 +57,14 @@ describe('bridge:deposit', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -60,10 +72,14 @@ describe('bridge:deposit', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -71,11 +87,16 @@ describe('bridge:deposit', () => { it('rejects invalid network value', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -83,11 +104,16 @@ describe('bridge:deposit', () => { it('rejects invalid address format', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow('is not a valid address') }) @@ -95,10 +121,14 @@ describe('bridge:deposit', () => { it('requires signing method (privateKey or useLedger)', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/commands/bridge/deposit.ts b/packages/cli/src/commands/bridge/deposit.ts index 552b7dbf23..d532ccec1d 100644 --- a/packages/cli/src/commands/bridge/deposit.ts +++ b/packages/cli/src/commands/bridge/deposit.ts @@ -114,11 +114,11 @@ export default class BridgeDeposit extends BaseCommand { }) if (depositReceipt.status === 'success') { - console.log( - '\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.' - ) + console.log('\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.') } else { - throw new Error('Deposit transaction failed. Please check the transaction on a block explorer.') + throw new Error( + 'Deposit transaction failed. Please check the transaction on a block explorer.' + ) } } diff --git a/packages/cli/src/commands/bridge/withdraw-finalize.ts b/packages/cli/src/commands/bridge/withdraw-finalize.ts index bf994c058c..b6a7f5150d 100644 --- a/packages/cli/src/commands/bridge/withdraw-finalize.ts +++ b/packages/cli/src/commands/bridge/withdraw-finalize.ts @@ -83,8 +83,7 @@ export default class BridgeWithdrawFinalize extends BaseCommand { const statusMessages: Record = { 'waiting-to-prove': 'The withdrawal has not been proven yet. Run bridge:withdraw-prove first.', - 'ready-to-prove': - 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', + 'ready-to-prove': 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', 'waiting-to-finalize': 'The 7-day challenge period has not passed yet. Please wait and try again later.', finalized: 'This withdrawal has already been finalized.', diff --git a/packages/cli/src/commands/bridge/withdraw-init.ts b/packages/cli/src/commands/bridge/withdraw-init.ts index 26d8117504..abec747a08 100644 --- a/packages/cli/src/commands/bridge/withdraw-init.ts +++ b/packages/cli/src/commands/bridge/withdraw-init.ts @@ -3,11 +3,7 @@ import { BaseCommand } from '../../base' import { displayViemTx } from '../../utils/cli' import { CustomFlags } from '../../utils/command' import { newCheckBuilder } from '../../utils/checks' -import { - BRIDGE_CONFIG, - L2_L1_MESSAGE_PASSER_ABI, - validateNetwork, -} from '../../utils/bridge' +import { BRIDGE_CONFIG, L2_L1_MESSAGE_PASSER_ABI, validateNetwork } from '../../utils/bridge' export default class BridgeWithdrawInit extends BaseCommand { static description = diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts index 47b6545487..7208c59b90 100644 --- a/packages/cli/src/commands/bridge/withdraw-prove.ts +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -112,7 +112,9 @@ export default class BridgeWithdrawProve extends BaseCommand { console.log('\nWithdrawal proof submitted! Next steps:') console.log(' 1. Wait 7 days for the challenge period to pass') console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') - console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') + console.log( + ' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...' + ) } private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { diff --git a/packages/cli/src/commands/bridge/withdraw-status.ts b/packages/cli/src/commands/bridge/withdraw-status.ts index 6b6e95abc4..3e82f5435e 100644 --- a/packages/cli/src/commands/bridge/withdraw-status.ts +++ b/packages/cli/src/commands/bridge/withdraw-status.ts @@ -81,7 +81,9 @@ export default class BridgeWithdrawStatus extends BaseCommand { }) ux.action.stop() - const statusInfo = WITHDRAWAL_STATUS_LABELS[status as keyof typeof WITHDRAWAL_STATUS_LABELS] || { + const statusInfo = WITHDRAWAL_STATUS_LABELS[ + status as keyof typeof WITHDRAWAL_STATUS_LABELS + ] || { label: status, description: 'Unknown status', } diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts index c4f943dc54..133b50bbea 100644 --- a/packages/cli/src/commands/bridge/withdraw.test.ts +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -30,9 +30,12 @@ describe('bridge:withdraw-init', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -40,9 +43,12 @@ describe('bridge:withdraw-init', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -50,9 +56,12 @@ describe('bridge:withdraw-init', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -60,11 +69,16 @@ describe('bridge:withdraw-init', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -72,10 +86,14 @@ describe('bridge:withdraw-init', () => { it('rejects invalid from address', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow('is not a valid address') }) @@ -95,11 +113,16 @@ describe('bridge:withdraw-prove', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -107,11 +130,16 @@ describe('bridge:withdraw-prove', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -119,11 +147,16 @@ describe('bridge:withdraw-prove', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -131,12 +164,18 @@ describe('bridge:withdraw-prove', () => { it('rejects invalid txHash format', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', 'not-a-hash', - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + 'not-a-hash', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -156,9 +195,12 @@ describe('bridge:withdraw-status', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -166,9 +208,12 @@ describe('bridge:withdraw-status', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -176,10 +221,14 @@ describe('bridge:withdraw-status', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -199,11 +248,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -211,11 +265,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -223,12 +282,18 @@ describe('bridge:withdraw-finalize', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index 003f09b170..32eca6721f 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -196,13 +196,11 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: - 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: - 'The proof has been submitted. The 7-day challenge period is in progress.', + description: 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From d8864d081b9b75d8bdee4b22b2933badc834993f Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 15:20:25 +0200 Subject: [PATCH 06/15] Add as any to unblock CI. --- packages/cli/src/utils/bridge.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index 32eca6721f..aa0be15852 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -110,7 +110,7 @@ export function getL2OpChain(network: BridgeNetwork) { return network === 'mainnet' ? celoL2 : celoSepoliaL2 } -export function createL1PublicClient(l1RpcUrl: string, network: BridgeNetwork) { +export function createL1PublicClient(l1RpcUrl: string, network: BridgeNetwork): any { const config = BRIDGE_CONFIG[network] return createPublicClient({ chain: config.l1Chain, @@ -196,11 +196,13 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: + 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: 'The proof has been submitted. The 7-day challenge period is in progress.', + description: + 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From 709a92eadd0ea445f0b700123aa79943cfa2e07f Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 15:20:33 +0200 Subject: [PATCH 07/15] Formatting. --- packages/cli/src/utils/bridge.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index aa0be15852..652812d95f 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -196,13 +196,11 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: - 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: - 'The proof has been submitted. The 7-day challenge period is in progress.', + description: 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From a7a9dd2783c7d9b66d89bee75c0100a5886bfecd Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 16:18:35 +0200 Subject: [PATCH 08/15] Copilot CR. --- .../cli/src/commands/bridge/deposit.test.ts | 90 +++----- packages/cli/src/commands/bridge/deposit.ts | 15 +- .../src/commands/bridge/withdraw-finalize.ts | 16 +- .../cli/src/commands/bridge/withdraw-init.ts | 33 ++- .../cli/src/commands/bridge/withdraw-prove.ts | 20 +- .../src/commands/bridge/withdraw-status.ts | 10 +- .../cli/src/commands/bridge/withdraw.test.ts | 195 ++++++------------ packages/cli/src/utils/bridge.ts | 26 ++- 8 files changed, 187 insertions(+), 218 deletions(-) diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts index 051fd7cf8f..07421d828b 100644 --- a/packages/cli/src/commands/bridge/deposit.test.ts +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -27,14 +27,10 @@ describe('bridge:deposit', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -42,14 +38,10 @@ describe('bridge:deposit', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -57,14 +49,10 @@ describe('bridge:deposit', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -72,14 +60,10 @@ describe('bridge:deposit', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -87,16 +71,11 @@ describe('bridge:deposit', () => { it('rejects invalid network value', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -104,16 +83,11 @@ describe('bridge:deposit', () => { it('rejects invalid address format', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - 'not-an-address', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow('is not a valid address') }) @@ -121,14 +95,10 @@ describe('bridge:deposit', () => { it('requires signing method (privateKey or useLedger)', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/commands/bridge/deposit.ts b/packages/cli/src/commands/bridge/deposit.ts index d532ccec1d..79b744c229 100644 --- a/packages/cli/src/commands/bridge/deposit.ts +++ b/packages/cli/src/commands/bridge/deposit.ts @@ -10,6 +10,7 @@ import { ERC20_APPROVE_ABI, OPTIMISM_PORTAL_DEPOSIT_ABI, validateNetwork, + verifyL1ChainId, type BridgeNetwork, } from '../../utils/bridge' @@ -70,6 +71,8 @@ export default class BridgeDeposit extends BaseCommand { transport: http(l1RpcUrl), }) + await verifyL1ChainId(l1Client, network) + // Step 1: Retrieve gas paying token (CELO address on L1) ux.action.start('Step 1/3: Retrieving CELO token address on L1') const [celoL1Address] = await l1Client.readContract({ @@ -95,6 +98,10 @@ export default class BridgeDeposit extends BaseCommand { ux.action.stop() printValueMap({ 'Approval txHash': approveReceipt.transactionHash }) + if (approveReceipt.status !== 'success') { + throw new Error('Approval transaction failed. Please check the transaction on a block explorer.') + } + // Step 3: Deposit CELO to L2 ux.action.start('Step 3/3: Depositing CELO to L2') const depositHash = await wallet.writeContract({ @@ -114,11 +121,11 @@ export default class BridgeDeposit extends BaseCommand { }) if (depositReceipt.status === 'success') { - console.log('\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.') - } else { - throw new Error( - 'Deposit transaction failed. Please check the transaction on a block explorer.' + console.log( + '\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.' ) + } else { + throw new Error('Deposit transaction failed. Please check the transaction on a block explorer.') } } diff --git a/packages/cli/src/commands/bridge/withdraw-finalize.ts b/packages/cli/src/commands/bridge/withdraw-finalize.ts index b6a7f5150d..31d572317a 100644 --- a/packages/cli/src/commands/bridge/withdraw-finalize.ts +++ b/packages/cli/src/commands/bridge/withdraw-finalize.ts @@ -11,6 +11,8 @@ import { validateNetwork, getL2OpChain, createL1PublicClient, + verifyL1ChainId, + verifyL2ChainId, type BridgeNetwork, } from '../../utils/bridge' @@ -62,8 +64,13 @@ export default class BridgeWithdrawFinalize extends BaseCommand { transport: http(l2NodeUrl), }).extend(publicActionsL2()) + await verifyL2ChainId(l2Client, network) + // Create L1 clients const l1Client = createL1PublicClient(l1RpcUrl, network) + + await verifyL1ChainId(l1Client, network) + const l1Wallet = await this.getL1WalletClient(res, l1RpcUrl, network) // Step 1: Get the withdrawal receipt @@ -83,7 +90,8 @@ export default class BridgeWithdrawFinalize extends BaseCommand { const statusMessages: Record = { 'waiting-to-prove': 'The withdrawal has not been proven yet. Run bridge:withdraw-prove first.', - 'ready-to-prove': 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', + 'ready-to-prove': + 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', 'waiting-to-finalize': 'The 7-day challenge period has not passed yet. Please wait and try again later.', finalized: 'This withdrawal has already been finalized.', @@ -111,7 +119,11 @@ export default class BridgeWithdrawFinalize extends BaseCommand { status: finalizeReceipt.status === 'success' ? 'Success' : 'Failed', }) - console.log('\nWithdrawal finalized! Your CELO has been sent to your L1 address.') + if (finalizeReceipt.status === 'success') { + console.log('\nWithdrawal finalized! Your CELO has been sent to your L1 address.') + } else { + throw new Error('Finalize transaction failed. Please check the transaction on a block explorer.') + } } private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { diff --git a/packages/cli/src/commands/bridge/withdraw-init.ts b/packages/cli/src/commands/bridge/withdraw-init.ts index abec747a08..1445a2a78c 100644 --- a/packages/cli/src/commands/bridge/withdraw-init.ts +++ b/packages/cli/src/commands/bridge/withdraw-init.ts @@ -1,9 +1,14 @@ import { Flags } from '@oclif/core' import { BaseCommand } from '../../base' -import { displayViemTx } from '../../utils/cli' +import { printValueMap } from '../../utils/cli' import { CustomFlags } from '../../utils/command' import { newCheckBuilder } from '../../utils/checks' -import { BRIDGE_CONFIG, L2_L1_MESSAGE_PASSER_ABI, validateNetwork } from '../../utils/bridge' +import { + BRIDGE_CONFIG, + L2_L1_MESSAGE_PASSER_ABI, + validateNetwork, + verifyL2ChainId, +} from '../../utils/bridge' export default class BridgeWithdrawInit extends BaseCommand { static description = @@ -48,6 +53,8 @@ export default class BridgeWithdrawInit extends BaseCommand { const network = validateNetwork(networkFlag) const config = BRIDGE_CONFIG[network] + await verifyL2ChainId(client, network) + await newCheckBuilder(this) .isNotSanctioned(from) .isNotSanctioned(to) @@ -58,24 +65,28 @@ export default class BridgeWithdrawInit extends BaseCommand { console.log('\nInitiating withdrawal from L2 to L1...') console.log('This sends your CELO to the L2→L1 message bridge.\n') - const txHash = wallet.writeContract({ + const hash = wallet.writeContract({ address: config.l2L1MessagePasser, abi: L2_L1_MESSAGE_PASSER_ABI, functionName: 'initiateWithdrawal', - // Type assertion needed: wallet client is created dynamically so TS can't infer chain/account args: [to, BigInt(0), '0x'], value: value, chain: client.chain, account: wallet.account!, } as any) as Promise<`0x${string}`> - await displayViemTx('InitiateWithdrawal', txHash, client) + const receipt = await client.waitForTransactionReceipt({ hash: await hash }) + printValueMap({ txHash: receipt.transactionHash }) - console.log('\nWithdrawal initiated! Save the transaction hash above.') - console.log('Next steps:') - console.log(' 1. Wait ~1 hour for the proof to become available') - console.log(' 2. Run: celocli bridge:withdraw-prove --txHash ...') - console.log(' 3. Wait 7 days for the challenge period to pass') - console.log(' 4. Run: celocli bridge:withdraw-finalize --txHash ...') + if (receipt.status === 'success') { + console.log('\nWithdrawal initiated! Save the transaction hash above.') + console.log('Next steps:') + console.log(' 1. Wait ~1 hour for the proof to become available') + console.log(' 2. Run: celocli bridge:withdraw-prove --txHash ' + receipt.transactionHash + ' ...') + console.log(' 3. Wait 7 days for the challenge period to pass') + console.log(' 4. Run: celocli bridge:withdraw-finalize --txHash ' + receipt.transactionHash + ' ...') + } else { + throw new Error('Withdrawal initiation failed. Please check the transaction on a block explorer.') + } } } diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts index 7208c59b90..3294727c58 100644 --- a/packages/cli/src/commands/bridge/withdraw-prove.ts +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -11,6 +11,8 @@ import { validateNetwork, getL2OpChain, createL1PublicClient, + verifyL1ChainId, + verifyL2ChainId, type BridgeNetwork, } from '../../utils/bridge' @@ -62,9 +64,13 @@ export default class BridgeWithdrawProve extends BaseCommand { transport: http(l2NodeUrl), }).extend(publicActionsL2()) + await verifyL2ChainId(l2Client, network) + // Create L1 public client with OP Stack extensions const l1Client = createL1PublicClient(l1RpcUrl, network) + await verifyL1ChainId(l1Client, network) + // Create L1 wallet client for submitting the prove tx const l1Wallet = await this.getL1WalletClient(res, l1RpcUrl, network) @@ -109,12 +115,14 @@ export default class BridgeWithdrawProve extends BaseCommand { status: proveReceipt.status === 'success' ? 'Success' : 'Failed', }) - console.log('\nWithdrawal proof submitted! Next steps:') - console.log(' 1. Wait 7 days for the challenge period to pass') - console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') - console.log( - ' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...' - ) + if (proveReceipt.status === 'success') { + console.log('\nWithdrawal proof submitted! Next steps:') + console.log(' 1. Wait 7 days for the challenge period to pass') + console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') + console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') + } else { + throw new Error('Prove transaction failed. Please check the transaction on a block explorer.') + } } private async getL1WalletClient(res: any, l1RpcUrl: string, network: BridgeNetwork) { diff --git a/packages/cli/src/commands/bridge/withdraw-status.ts b/packages/cli/src/commands/bridge/withdraw-status.ts index 3e82f5435e..69f7f948f5 100644 --- a/packages/cli/src/commands/bridge/withdraw-status.ts +++ b/packages/cli/src/commands/bridge/withdraw-status.ts @@ -9,6 +9,8 @@ import { validateNetwork, getL2OpChain, createL1PublicClient, + verifyL1ChainId, + verifyL2ChainId, } from '../../utils/bridge' export default class BridgeWithdrawStatus extends BaseCommand { @@ -59,9 +61,13 @@ export default class BridgeWithdrawStatus extends BaseCommand { transport: http(l2NodeUrl), }).extend(publicActionsL2()) + await verifyL2ChainId(l2Client, network) + // Create L1 client with OP Stack extensions const l1Client = createL1PublicClient(l1RpcUrl, network) + await verifyL1ChainId(l1Client, network) + // Get the receipt ux.action.start('Fetching withdrawal transaction') const receipt = await l2Client.getTransactionReceipt({ hash: txHash as `0x${string}` }) @@ -81,9 +87,7 @@ export default class BridgeWithdrawStatus extends BaseCommand { }) ux.action.stop() - const statusInfo = WITHDRAWAL_STATUS_LABELS[ - status as keyof typeof WITHDRAWAL_STATUS_LABELS - ] || { + const statusInfo = WITHDRAWAL_STATUS_LABELS[status as keyof typeof WITHDRAWAL_STATUS_LABELS] || { label: status, description: 'Unknown status', } diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts index 133b50bbea..c4f943dc54 100644 --- a/packages/cli/src/commands/bridge/withdraw.test.ts +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -30,12 +30,9 @@ describe('bridge:withdraw-init', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -43,12 +40,9 @@ describe('bridge:withdraw-init', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -56,12 +50,9 @@ describe('bridge:withdraw-init', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--node', - 'celo-sepolia', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -69,16 +60,11 @@ describe('bridge:withdraw-init', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'goerli', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -86,14 +72,10 @@ describe('bridge:withdraw-init', () => { it('rejects invalid from address', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - 'not-an-address', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow('is not a valid address') }) @@ -113,16 +95,11 @@ describe('bridge:withdraw-prove', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -130,16 +107,11 @@ describe('bridge:withdraw-prove', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -147,16 +119,11 @@ describe('bridge:withdraw-prove', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -164,18 +131,12 @@ describe('bridge:withdraw-prove', () => { it('rejects invalid txHash format', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - 'not-a-hash', - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', 'not-a-hash', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -195,12 +156,9 @@ describe('bridge:withdraw-status', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -208,12 +166,9 @@ describe('bridge:withdraw-status', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -221,14 +176,10 @@ describe('bridge:withdraw-status', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -248,16 +199,11 @@ describe('bridge:withdraw-finalize', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -265,16 +211,11 @@ describe('bridge:withdraw-finalize', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -282,18 +223,12 @@ describe('bridge:withdraw-finalize', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index 652812d95f..758964c2d7 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -110,6 +110,26 @@ export function getL2OpChain(network: BridgeNetwork) { return network === 'mainnet' ? celoL2 : celoSepoliaL2 } +export async function verifyL2ChainId(client: { getChainId: () => Promise }, network: BridgeNetwork) { + const expectedChainId = getL2OpChain(network).id + const actualChainId = await client.getChainId() + if (actualChainId !== expectedChainId) { + throw new Error( + `L2 node chain ID mismatch: --network ${network} expects chain ${expectedChainId} but --node is connected to chain ${actualChainId}. Ensure --node points to the correct L2 network.` + ) + } +} + +export async function verifyL1ChainId(client: { getChainId: () => Promise }, network: BridgeNetwork) { + const expectedChainId = BRIDGE_CONFIG[network].l1Chain.id + const actualChainId = await client.getChainId() + if (actualChainId !== expectedChainId) { + throw new Error( + `L1 RPC chain ID mismatch: --network ${network} expects chain ${expectedChainId} but --l1RpcUrl is connected to chain ${actualChainId}. Ensure --l1RpcUrl points to the correct L1 network.` + ) + } +} + export function createL1PublicClient(l1RpcUrl: string, network: BridgeNetwork): any { const config = BRIDGE_CONFIG[network] return createPublicClient({ @@ -196,11 +216,13 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: + 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: 'The proof has been submitted. The 7-day challenge period is in progress.', + description: + 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From df95beda16d6432d62df2e942ab4672c5115217b Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 16:18:50 +0200 Subject: [PATCH 09/15] Formatting. --- .../cli/src/commands/bridge/deposit.test.ts | 90 +++++--- packages/cli/src/commands/bridge/deposit.ts | 12 +- .../src/commands/bridge/withdraw-finalize.ts | 7 +- .../cli/src/commands/bridge/withdraw-init.ts | 12 +- .../cli/src/commands/bridge/withdraw-prove.ts | 4 +- .../src/commands/bridge/withdraw-status.ts | 4 +- .../cli/src/commands/bridge/withdraw.test.ts | 195 ++++++++++++------ packages/cli/src/utils/bridge.ts | 16 +- 8 files changed, 226 insertions(+), 114 deletions(-) diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts index 07421d828b..051fd7cf8f 100644 --- a/packages/cli/src/commands/bridge/deposit.test.ts +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -27,10 +27,14 @@ describe('bridge:deposit', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -38,10 +42,14 @@ describe('bridge:deposit', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -49,10 +57,14 @@ describe('bridge:deposit', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -60,10 +72,14 @@ describe('bridge:deposit', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -71,11 +87,16 @@ describe('bridge:deposit', () => { it('rejects invalid network value', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -83,11 +104,16 @@ describe('bridge:deposit', () => { it('rejects invalid address format', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow('is not a valid address') }) @@ -95,10 +121,14 @@ describe('bridge:deposit', () => { it('requires signing method (privateKey or useLedger)', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/commands/bridge/deposit.ts b/packages/cli/src/commands/bridge/deposit.ts index 79b744c229..bba90555a2 100644 --- a/packages/cli/src/commands/bridge/deposit.ts +++ b/packages/cli/src/commands/bridge/deposit.ts @@ -99,7 +99,9 @@ export default class BridgeDeposit extends BaseCommand { printValueMap({ 'Approval txHash': approveReceipt.transactionHash }) if (approveReceipt.status !== 'success') { - throw new Error('Approval transaction failed. Please check the transaction on a block explorer.') + throw new Error( + 'Approval transaction failed. Please check the transaction on a block explorer.' + ) } // Step 3: Deposit CELO to L2 @@ -121,11 +123,11 @@ export default class BridgeDeposit extends BaseCommand { }) if (depositReceipt.status === 'success') { - console.log( - '\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.' - ) + console.log('\nDeposit initiated! Your CELO should appear on L2 in approximately 15 minutes.') } else { - throw new Error('Deposit transaction failed. Please check the transaction on a block explorer.') + throw new Error( + 'Deposit transaction failed. Please check the transaction on a block explorer.' + ) } } diff --git a/packages/cli/src/commands/bridge/withdraw-finalize.ts b/packages/cli/src/commands/bridge/withdraw-finalize.ts index 31d572317a..90506e77a9 100644 --- a/packages/cli/src/commands/bridge/withdraw-finalize.ts +++ b/packages/cli/src/commands/bridge/withdraw-finalize.ts @@ -90,8 +90,7 @@ export default class BridgeWithdrawFinalize extends BaseCommand { const statusMessages: Record = { 'waiting-to-prove': 'The withdrawal has not been proven yet. Run bridge:withdraw-prove first.', - 'ready-to-prove': - 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', + 'ready-to-prove': 'The withdrawal needs to be proven first. Run bridge:withdraw-prove.', 'waiting-to-finalize': 'The 7-day challenge period has not passed yet. Please wait and try again later.', finalized: 'This withdrawal has already been finalized.', @@ -122,7 +121,9 @@ export default class BridgeWithdrawFinalize extends BaseCommand { if (finalizeReceipt.status === 'success') { console.log('\nWithdrawal finalized! Your CELO has been sent to your L1 address.') } else { - throw new Error('Finalize transaction failed. Please check the transaction on a block explorer.') + throw new Error( + 'Finalize transaction failed. Please check the transaction on a block explorer.' + ) } } diff --git a/packages/cli/src/commands/bridge/withdraw-init.ts b/packages/cli/src/commands/bridge/withdraw-init.ts index 1445a2a78c..b50424ea15 100644 --- a/packages/cli/src/commands/bridge/withdraw-init.ts +++ b/packages/cli/src/commands/bridge/withdraw-init.ts @@ -82,11 +82,17 @@ export default class BridgeWithdrawInit extends BaseCommand { console.log('\nWithdrawal initiated! Save the transaction hash above.') console.log('Next steps:') console.log(' 1. Wait ~1 hour for the proof to become available') - console.log(' 2. Run: celocli bridge:withdraw-prove --txHash ' + receipt.transactionHash + ' ...') + console.log( + ' 2. Run: celocli bridge:withdraw-prove --txHash ' + receipt.transactionHash + ' ...' + ) console.log(' 3. Wait 7 days for the challenge period to pass') - console.log(' 4. Run: celocli bridge:withdraw-finalize --txHash ' + receipt.transactionHash + ' ...') + console.log( + ' 4. Run: celocli bridge:withdraw-finalize --txHash ' + receipt.transactionHash + ' ...' + ) } else { - throw new Error('Withdrawal initiation failed. Please check the transaction on a block explorer.') + throw new Error( + 'Withdrawal initiation failed. Please check the transaction on a block explorer.' + ) } } } diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts index 3294727c58..54e01c9f78 100644 --- a/packages/cli/src/commands/bridge/withdraw-prove.ts +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -119,7 +119,9 @@ export default class BridgeWithdrawProve extends BaseCommand { console.log('\nWithdrawal proof submitted! Next steps:') console.log(' 1. Wait 7 days for the challenge period to pass') console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') - console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') + console.log( + ' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...' + ) } else { throw new Error('Prove transaction failed. Please check the transaction on a block explorer.') } diff --git a/packages/cli/src/commands/bridge/withdraw-status.ts b/packages/cli/src/commands/bridge/withdraw-status.ts index 69f7f948f5..2dcba6478a 100644 --- a/packages/cli/src/commands/bridge/withdraw-status.ts +++ b/packages/cli/src/commands/bridge/withdraw-status.ts @@ -87,7 +87,9 @@ export default class BridgeWithdrawStatus extends BaseCommand { }) ux.action.stop() - const statusInfo = WITHDRAWAL_STATUS_LABELS[status as keyof typeof WITHDRAWAL_STATUS_LABELS] || { + const statusInfo = WITHDRAWAL_STATUS_LABELS[ + status as keyof typeof WITHDRAWAL_STATUS_LABELS + ] || { label: status, description: 'Unknown status', } diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts index c4f943dc54..133b50bbea 100644 --- a/packages/cli/src/commands/bridge/withdraw.test.ts +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -30,9 +30,12 @@ describe('bridge:withdraw-init', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -40,9 +43,12 @@ describe('bridge:withdraw-init', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -50,9 +56,12 @@ describe('bridge:withdraw-init', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -60,11 +69,16 @@ describe('bridge:withdraw-init', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -72,10 +86,14 @@ describe('bridge:withdraw-init', () => { it('rejects invalid from address', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow('is not a valid address') }) @@ -95,11 +113,16 @@ describe('bridge:withdraw-prove', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -107,11 +130,16 @@ describe('bridge:withdraw-prove', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -119,11 +147,16 @@ describe('bridge:withdraw-prove', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -131,12 +164,18 @@ describe('bridge:withdraw-prove', () => { it('rejects invalid txHash format', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', 'not-a-hash', - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + 'not-a-hash', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -156,9 +195,12 @@ describe('bridge:withdraw-status', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -166,9 +208,12 @@ describe('bridge:withdraw-status', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -176,10 +221,14 @@ describe('bridge:withdraw-status', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -199,11 +248,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -211,11 +265,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -223,12 +282,18 @@ describe('bridge:withdraw-finalize', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index 758964c2d7..354e7b0e3d 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -110,7 +110,10 @@ export function getL2OpChain(network: BridgeNetwork) { return network === 'mainnet' ? celoL2 : celoSepoliaL2 } -export async function verifyL2ChainId(client: { getChainId: () => Promise }, network: BridgeNetwork) { +export async function verifyL2ChainId( + client: { getChainId: () => Promise }, + network: BridgeNetwork +) { const expectedChainId = getL2OpChain(network).id const actualChainId = await client.getChainId() if (actualChainId !== expectedChainId) { @@ -120,7 +123,10 @@ export async function verifyL2ChainId(client: { getChainId: () => Promise Promise }, network: BridgeNetwork) { +export async function verifyL1ChainId( + client: { getChainId: () => Promise }, + network: BridgeNetwork +) { const expectedChainId = BRIDGE_CONFIG[network].l1Chain.id const actualChainId = await client.getChainId() if (actualChainId !== expectedChainId) { @@ -216,13 +222,11 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: - 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: - 'The proof has been submitted. The 7-day challenge period is in progress.', + description: 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From 1463195ca896661c4676fbfacb193ed6a833c021 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 16:58:09 +0200 Subject: [PATCH 10/15] Fix CI. --- .../cli/src/commands/bridge/deposit.test.ts | 102 +++----- .../cli/src/commands/bridge/withdraw.test.ts | 243 ++++++++---------- 2 files changed, 140 insertions(+), 205 deletions(-) diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts index 051fd7cf8f..590a549f2c 100644 --- a/packages/cli/src/commands/bridge/deposit.test.ts +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -15,9 +15,15 @@ jest.setTimeout(15000) describe('bridge:deposit', () => { beforeEach(() => { - jest.spyOn(console, 'log').mockImplementation(() => {}) - jest.spyOn(console, 'error').mockImplementation(() => {}) - jest.spyOn(console, 'info').mockImplementation(() => {}) + jest.spyOn(console, 'log').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'error').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'info').mockImplementation(() => { + // noop + }) }) afterEach(() => { @@ -27,14 +33,10 @@ describe('bridge:deposit', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -42,14 +44,10 @@ describe('bridge:deposit', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -57,14 +55,10 @@ describe('bridge:deposit', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -72,14 +66,10 @@ describe('bridge:deposit', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -87,16 +77,11 @@ describe('bridge:deposit', () => { it('rejects invalid network value', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -104,16 +89,11 @@ describe('bridge:deposit', () => { it('rejects invalid address format', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - 'not-an-address', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '-k', - '0x' + '1'.repeat(64), + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow('is not a valid address') }) @@ -121,14 +101,10 @@ describe('bridge:deposit', () => { it('requires signing method (privateKey or useLedger)', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts index 133b50bbea..3f48b1fb0e 100644 --- a/packages/cli/src/commands/bridge/withdraw.test.ts +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -18,9 +18,15 @@ jest.setTimeout(15000) describe('bridge:withdraw-init', () => { beforeEach(() => { - jest.spyOn(console, 'log').mockImplementation(() => {}) - jest.spyOn(console, 'error').mockImplementation(() => {}) - jest.spyOn(console, 'info').mockImplementation(() => {}) + jest.spyOn(console, 'log').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'error').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'info').mockImplementation(() => { + // noop + }) }) afterEach(() => { @@ -30,12 +36,9 @@ describe('bridge:withdraw-init', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -43,12 +46,9 @@ describe('bridge:withdraw-init', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -56,12 +56,9 @@ describe('bridge:withdraw-init', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--node', - 'celo-sepolia', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -69,16 +66,11 @@ describe('bridge:withdraw-init', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', - '1000000000000000000', - '--network', - 'goerli', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', '1000000000000000000', + '--network', 'goerli', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -86,14 +78,10 @@ describe('bridge:withdraw-init', () => { it('rejects invalid from address', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', - 'not-an-address', - '--value', - '1000000000000000000', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--from', 'not-an-address', + '--value', '1000000000000000000', + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow('is not a valid address') }) @@ -101,9 +89,15 @@ describe('bridge:withdraw-init', () => { describe('bridge:withdraw-prove', () => { beforeEach(() => { - jest.spyOn(console, 'log').mockImplementation(() => {}) - jest.spyOn(console, 'error').mockImplementation(() => {}) - jest.spyOn(console, 'info').mockImplementation(() => {}) + jest.spyOn(console, 'log').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'error').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'info').mockImplementation(() => { + // noop + }) }) afterEach(() => { @@ -113,16 +107,11 @@ describe('bridge:withdraw-prove', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -130,16 +119,11 @@ describe('bridge:withdraw-prove', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -147,16 +131,11 @@ describe('bridge:withdraw-prove', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -164,18 +143,12 @@ describe('bridge:withdraw-prove', () => { it('rejects invalid txHash format', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', - 'not-a-hash', - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', 'not-a-hash', + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -183,9 +156,15 @@ describe('bridge:withdraw-prove', () => { describe('bridge:withdraw-status', () => { beforeEach(() => { - jest.spyOn(console, 'log').mockImplementation(() => {}) - jest.spyOn(console, 'error').mockImplementation(() => {}) - jest.spyOn(console, 'info').mockImplementation(() => {}) + jest.spyOn(console, 'log').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'error').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'info').mockImplementation(() => { + // noop + }) }) afterEach(() => { @@ -195,12 +174,9 @@ describe('bridge:withdraw-status', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -208,12 +184,9 @@ describe('bridge:withdraw-status', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--node', - 'celo-sepolia', + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -221,14 +194,10 @@ describe('bridge:withdraw-status', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -236,9 +205,15 @@ describe('bridge:withdraw-status', () => { describe('bridge:withdraw-finalize', () => { beforeEach(() => { - jest.spyOn(console, 'log').mockImplementation(() => {}) - jest.spyOn(console, 'error').mockImplementation(() => {}) - jest.spyOn(console, 'info').mockImplementation(() => {}) + jest.spyOn(console, 'log').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'error').mockImplementation(() => { + // noop + }) + jest.spyOn(console, 'info').mockImplementation(() => { + // noop + }) }) afterEach(() => { @@ -248,16 +223,11 @@ describe('bridge:withdraw-finalize', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -265,16 +235,11 @@ describe('bridge:withdraw-finalize', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--network', - 'sepolia', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--network', 'sepolia', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -282,18 +247,12 @@ describe('bridge:withdraw-finalize', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', - '0x' + 'a'.repeat(64), - '--from', - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', - 'goerli', - '--l1RpcUrl', - 'https://eth-sepolia.example.com', - '--node', - 'celo-sepolia', - '-k', - '0x' + '1'.repeat(64), + '--txHash', '0x' + 'a'.repeat(64), + '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', 'goerli', + '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--node', 'celo-sepolia', + '-k', '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) From 6017ecd28fe5e794f005861157ae8666ca2e87a8 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 20 Apr 2026 16:58:19 +0200 Subject: [PATCH 11/15] Formatting. --- .../cli/src/commands/bridge/deposit.test.ts | 90 +++++--- .../cli/src/commands/bridge/withdraw.test.ts | 195 ++++++++++++------ 2 files changed, 190 insertions(+), 95 deletions(-) diff --git a/packages/cli/src/commands/bridge/deposit.test.ts b/packages/cli/src/commands/bridge/deposit.test.ts index 590a549f2c..556244d547 100644 --- a/packages/cli/src/commands/bridge/deposit.test.ts +++ b/packages/cli/src/commands/bridge/deposit.test.ts @@ -33,10 +33,14 @@ describe('bridge:deposit', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -44,10 +48,14 @@ describe('bridge:deposit', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -55,10 +63,14 @@ describe('bridge:deposit', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -66,10 +78,14 @@ describe('bridge:deposit', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -77,11 +93,16 @@ describe('bridge:deposit', () => { it('rejects invalid network value', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -89,11 +110,16 @@ describe('bridge:deposit', () => { it('rejects invalid address format', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '-k', '0x' + '1'.repeat(64), + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow('is not a valid address') }) @@ -101,10 +127,14 @@ describe('bridge:deposit', () => { it('requires signing method (privateKey or useLedger)', async () => { await expect( testLocally(BridgeDeposit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', ]) ).rejects.toThrow() }) diff --git a/packages/cli/src/commands/bridge/withdraw.test.ts b/packages/cli/src/commands/bridge/withdraw.test.ts index 3f48b1fb0e..f016bc235a 100644 --- a/packages/cli/src/commands/bridge/withdraw.test.ts +++ b/packages/cli/src/commands/bridge/withdraw.test.ts @@ -36,9 +36,12 @@ describe('bridge:withdraw-init', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -46,9 +49,12 @@ describe('bridge:withdraw-init', () => { it('requires --value flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -56,9 +62,12 @@ describe('bridge:withdraw-init', () => { it('requires --network flag', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--node', 'celo-sepolia', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -66,11 +75,16 @@ describe('bridge:withdraw-init', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--value', '1000000000000000000', - '--network', 'goerli', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--value', + '1000000000000000000', + '--network', + 'goerli', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -78,10 +92,14 @@ describe('bridge:withdraw-init', () => { it('rejects invalid from address', async () => { await expect( testLocally(BridgeWithdrawInit, [ - '--from', 'not-an-address', - '--value', '1000000000000000000', - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--from', + 'not-an-address', + '--value', + '1000000000000000000', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow('is not a valid address') }) @@ -107,11 +125,16 @@ describe('bridge:withdraw-prove', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -119,11 +142,16 @@ describe('bridge:withdraw-prove', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -131,11 +159,16 @@ describe('bridge:withdraw-prove', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -143,12 +176,18 @@ describe('bridge:withdraw-prove', () => { it('rejects invalid txHash format', async () => { await expect( testLocally(BridgeWithdrawProve, [ - '--txHash', 'not-a-hash', - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + 'not-a-hash', + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -174,9 +213,12 @@ describe('bridge:withdraw-status', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -184,9 +226,12 @@ describe('bridge:withdraw-status', () => { it('requires --l1RpcUrl flag', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -194,10 +239,14 @@ describe('bridge:withdraw-status', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawStatus, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', ]) ).rejects.toThrow() }) @@ -223,11 +272,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --txHash flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -235,11 +289,16 @@ describe('bridge:withdraw-finalize', () => { it('requires --from flag', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--network', 'sepolia', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--network', + 'sepolia', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) @@ -247,12 +306,18 @@ describe('bridge:withdraw-finalize', () => { it('rejects invalid network', async () => { await expect( testLocally(BridgeWithdrawFinalize, [ - '--txHash', '0x' + 'a'.repeat(64), - '--from', '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '--network', 'goerli', - '--l1RpcUrl', 'https://eth-sepolia.example.com', - '--node', 'celo-sepolia', - '-k', '0x' + '1'.repeat(64), + '--txHash', + '0x' + 'a'.repeat(64), + '--from', + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '--network', + 'goerli', + '--l1RpcUrl', + 'https://eth-sepolia.example.com', + '--node', + 'celo-sepolia', + '-k', + '0x' + '1'.repeat(64), ]) ).rejects.toThrow() }) From 768a93c86c6f253dc6364d011adeeb71a4be596d Mon Sep 17 00:00:00 2001 From: Mc01 Date: Tue, 21 Apr 2026 10:21:05 +0200 Subject: [PATCH 12/15] Linting. --- packages/cli/src/utils/bridge.test.ts | 2 +- packages/cli/src/utils/bridge.ts | 28 ++++++++++++--------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/utils/bridge.test.ts b/packages/cli/src/utils/bridge.test.ts index d6fc8c4834..37e4edc198 100644 --- a/packages/cli/src/utils/bridge.test.ts +++ b/packages/cli/src/utils/bridge.test.ts @@ -55,7 +55,7 @@ describe('bridge utils', () => { expect(WITHDRAWAL_STATUS_LABELS['ready-to-prove']).toBeDefined() expect(WITHDRAWAL_STATUS_LABELS['waiting-to-finalize']).toBeDefined() expect(WITHDRAWAL_STATUS_LABELS['ready-to-finalize']).toBeDefined() - expect(WITHDRAWAL_STATUS_LABELS['finalized']).toBeDefined() + expect(WITHDRAWAL_STATUS_LABELS.finalized).toBeDefined() }) it('each status has label and description', () => { diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index 354e7b0e3d..380f72c446 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -52,13 +52,13 @@ const celoL2 = /*#__PURE__*/ defineChain({ blockCreated: 13112599, }, portal: { - [1]: { address: '0xc5c5D157928BDBD2ACf6d0777626b6C75a9EAEDC' }, + 1: { address: '0xc5c5D157928BDBD2ACf6d0777626b6C75a9EAEDC' }, }, disputeGameFactory: { - [1]: { address: '0xFbAC162162f4009Bb007C6DeBC36B1dAC10aF683' }, + 1: { address: '0xFbAC162162f4009Bb007C6DeBC36B1dAC10aF683' }, }, l1StandardBridge: { - [1]: { address: '0x9C4955b92F34148dbcfDCD82e9c9eCe5CF2badfe' }, + 1: { address: '0x9C4955b92F34148dbcfDCD82e9c9eCe5CF2badfe' }, }, }, sourceId: 1, @@ -86,13 +86,13 @@ const celoSepoliaL2 = /*#__PURE__*/ defineChain({ blockCreated: 1, }, portal: { - [11155111]: { address: '0x44ae3d41a335a7d05eb533029917aad35662dcc2', blockCreated: 8825790 }, + 11155111: { address: '0x44ae3d41a335a7d05eb533029917aad35662dcc2', blockCreated: 8825790 }, }, disputeGameFactory: { - [11155111]: { address: '0x57c45d82d1a995f1e135b8d7edc0a6bb5211cfaa', blockCreated: 8825790 }, + 11155111: { address: '0x57c45d82d1a995f1e135b8d7edc0a6bb5211cfaa', blockCreated: 8825790 }, }, l1StandardBridge: { - [11155111]: { address: '0xec18a3c30131a0db4246e785355fbc16e2eaf408', blockCreated: 8825790 }, + 11155111: { address: '0xec18a3c30131a0db4246e785355fbc16e2eaf408', blockCreated: 8825790 }, }, }, sourceId: 11155111, @@ -110,10 +110,7 @@ export function getL2OpChain(network: BridgeNetwork) { return network === 'mainnet' ? celoL2 : celoSepoliaL2 } -export async function verifyL2ChainId( - client: { getChainId: () => Promise }, - network: BridgeNetwork -) { +export async function verifyL2ChainId(client: { getChainId: () => Promise }, network: BridgeNetwork) { const expectedChainId = getL2OpChain(network).id const actualChainId = await client.getChainId() if (actualChainId !== expectedChainId) { @@ -123,10 +120,7 @@ export async function verifyL2ChainId( } } -export async function verifyL1ChainId( - client: { getChainId: () => Promise }, - network: BridgeNetwork -) { +export async function verifyL1ChainId(client: { getChainId: () => Promise }, network: BridgeNetwork) { const expectedChainId = BRIDGE_CONFIG[network].l1Chain.id const actualChainId = await client.getChainId() if (actualChainId !== expectedChainId) { @@ -222,11 +216,13 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: + 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: 'The proof has been submitted. The 7-day challenge period is in progress.', + description: + 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From 9cde87409c4ea7f22312cc1812e58f6435f6912f Mon Sep 17 00:00:00 2001 From: Mc01 Date: Tue, 21 Apr 2026 10:21:15 +0200 Subject: [PATCH 13/15] Formatting. --- packages/cli/src/utils/bridge.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/utils/bridge.ts b/packages/cli/src/utils/bridge.ts index 380f72c446..3062a3bfd0 100644 --- a/packages/cli/src/utils/bridge.ts +++ b/packages/cli/src/utils/bridge.ts @@ -110,7 +110,10 @@ export function getL2OpChain(network: BridgeNetwork) { return network === 'mainnet' ? celoL2 : celoSepoliaL2 } -export async function verifyL2ChainId(client: { getChainId: () => Promise }, network: BridgeNetwork) { +export async function verifyL2ChainId( + client: { getChainId: () => Promise }, + network: BridgeNetwork +) { const expectedChainId = getL2OpChain(network).id const actualChainId = await client.getChainId() if (actualChainId !== expectedChainId) { @@ -120,7 +123,10 @@ export async function verifyL2ChainId(client: { getChainId: () => Promise Promise }, network: BridgeNetwork) { +export async function verifyL1ChainId( + client: { getChainId: () => Promise }, + network: BridgeNetwork +) { const expectedChainId = BRIDGE_CONFIG[network].l1Chain.id const actualChainId = await client.getChainId() if (actualChainId !== expectedChainId) { @@ -216,13 +222,11 @@ export const WITHDRAWAL_STATUS_LABELS: Record< }, 'ready-to-prove': { label: 'Ready to Prove', - description: - 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', + description: 'The proof is available. You can now submit it on L1 with bridge:withdraw-prove.', }, 'waiting-to-finalize': { label: 'Waiting to Finalize', - description: - 'The proof has been submitted. The 7-day challenge period is in progress.', + description: 'The proof has been submitted. The 7-day challenge period is in progress.', }, 'ready-to-finalize': { label: 'Ready to Finalize', From 031c7c019fc6060c5652ee3d463b275e4afcf136 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Tue, 21 Apr 2026 10:38:10 +0200 Subject: [PATCH 14/15] Handle edge case. Increase coverage. --- .../cli/src/commands/bridge/withdraw-prove.ts | 33 +++++--- packages/cli/src/utils/bridge.test.ts | 78 ++++++++++++++++++- 2 files changed, 101 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts index 54e01c9f78..312ee6bd1c 100644 --- a/packages/cli/src/commands/bridge/withdraw-prove.ts +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -83,8 +83,25 @@ export default class BridgeWithdrawProve extends BaseCommand { status: receipt.status, }) - // Step 2: Check/wait for proof readiness - ux.action.start('Step 2/4: Waiting for proof to become available (this may take up to 1 hour)') + // Step 2: Check if already proven + ux.action.start('Step 2/5: Checking withdrawal status') + const status = await l1Client.getWithdrawalStatus({ + receipt, + targetChain: l2Chain as any, + }) + ux.action.stop() + + if (status !== 'ready-to-prove' && status !== 'waiting-to-prove') { + const messages: Record = { + 'waiting-to-finalize': 'This withdrawal has already been proven. Wait for the 7-day challenge period to pass, then run bridge:withdraw-finalize.', + 'ready-to-finalize': 'This withdrawal has already been proven and is ready to finalize. Run bridge:withdraw-finalize.', + finalized: 'This withdrawal has already been finalized.', + } + throw new Error(messages[status] || `Unexpected withdrawal status: ${status}`) + } + + // Step 3: Wait for proof readiness + ux.action.start('Step 3/5: Waiting for proof to become available (this may take up to 1 hour)') const { output, withdrawal } = await l1Client.waitToProve({ receipt, targetChain: l2Chain as any, @@ -92,8 +109,8 @@ export default class BridgeWithdrawProve extends BaseCommand { ux.action.stop() console.log('Proof is ready!') - // Step 3: Build the proof - ux.action.start('Step 3/4: Building withdrawal proof') + // Step 4: Build the proof + ux.action.start('Step 4/5: Building withdrawal proof') const proveArgs = await l2Client.buildProveWithdrawal({ output, withdrawal, @@ -101,8 +118,8 @@ export default class BridgeWithdrawProve extends BaseCommand { ux.action.stop() console.log('Proof built successfully.') - // Step 4: Submit the prove transaction on L1 - ux.action.start('Step 4/4: Submitting proof to L1') + // Step 5: Submit the prove transaction on L1 + ux.action.start('Step 5/5: Submitting proof to L1') const proveHash = await l1Wallet.proveWithdrawal(proveArgs) const proveReceipt = await createPublicClient({ chain: BRIDGE_CONFIG[network].l1Chain, @@ -119,9 +136,7 @@ export default class BridgeWithdrawProve extends BaseCommand { console.log('\nWithdrawal proof submitted! Next steps:') console.log(' 1. Wait 7 days for the challenge period to pass') console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') - console.log( - ' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...' - ) + console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') } else { throw new Error('Prove transaction failed. Please check the transaction on a block explorer.') } diff --git a/packages/cli/src/utils/bridge.test.ts b/packages/cli/src/utils/bridge.test.ts index 37e4edc198..5409cac56f 100644 --- a/packages/cli/src/utils/bridge.test.ts +++ b/packages/cli/src/utils/bridge.test.ts @@ -1,4 +1,12 @@ -import { validateNetwork, BRIDGE_CONFIG, WITHDRAWAL_STATUS_LABELS, getL2OpChain } from './bridge' +import { + validateNetwork, + BRIDGE_CONFIG, + WITHDRAWAL_STATUS_LABELS, + getL2OpChain, + createL1PublicClient, + verifyL2ChainId, + verifyL1ChainId, +} from './bridge' describe('bridge utils', () => { describe('validateNetwork', () => { @@ -65,4 +73,72 @@ describe('bridge utils', () => { }) }) }) + + describe('verifyL2ChainId', () => { + it('passes when chain ID matches mainnet', async () => { + const mockClient = { getChainId: async () => 42220 } + await expect(verifyL2ChainId(mockClient, 'mainnet')).resolves.toBeUndefined() + }) + + it('passes when chain ID matches sepolia', async () => { + const mockClient = { getChainId: async () => 11142220 } + await expect(verifyL2ChainId(mockClient, 'sepolia')).resolves.toBeUndefined() + }) + + it('throws when chain ID mismatches', async () => { + const mockClient = { getChainId: async () => 42220 } + await expect(verifyL2ChainId(mockClient, 'sepolia')).rejects.toThrow( + 'L2 node chain ID mismatch' + ) + }) + + it('includes expected and actual chain IDs in error', async () => { + const mockClient = { getChainId: async () => 99999 } + await expect(verifyL2ChainId(mockClient, 'mainnet')).rejects.toThrow( + 'expects chain 42220 but --node is connected to chain 99999' + ) + }) + }) + + describe('verifyL1ChainId', () => { + it('passes when chain ID matches mainnet (Ethereum)', async () => { + const mockClient = { getChainId: async () => 1 } + await expect(verifyL1ChainId(mockClient, 'mainnet')).resolves.toBeUndefined() + }) + + it('passes when chain ID matches sepolia', async () => { + const mockClient = { getChainId: async () => 11155111 } + await expect(verifyL1ChainId(mockClient, 'sepolia')).resolves.toBeUndefined() + }) + + it('throws when chain ID mismatches', async () => { + const mockClient = { getChainId: async () => 11155111 } + await expect(verifyL1ChainId(mockClient, 'mainnet')).rejects.toThrow( + 'L1 RPC chain ID mismatch' + ) + }) + + it('includes expected and actual chain IDs in error', async () => { + const mockClient = { getChainId: async () => 5 } + await expect(verifyL1ChainId(mockClient, 'sepolia')).rejects.toThrow( + 'expects chain 11155111 but --l1RpcUrl is connected to chain 5' + ) + }) + }) + + describe('createL1PublicClient', () => { + it('creates a client with OP Stack L1 actions for mainnet', () => { + const client = createL1PublicClient('https://eth.example.com', 'mainnet') + expect(client).toBeDefined() + expect(typeof client.getWithdrawalStatus).toBe('function') + expect(typeof client.waitToProve).toBe('function') + }) + + it('creates a client with OP Stack L1 actions for sepolia', () => { + const client = createL1PublicClient('https://eth-sepolia.example.com', 'sepolia') + expect(client).toBeDefined() + expect(typeof client.getWithdrawalStatus).toBe('function') + expect(typeof client.waitToProve).toBe('function') + }) + }) }) From 33d7414ffa837e41e88094b2d925e83b2e36388e Mon Sep 17 00:00:00 2001 From: Mc01 Date: Tue, 21 Apr 2026 10:38:20 +0200 Subject: [PATCH 15/15] Formatting. --- packages/cli/src/commands/bridge/withdraw-prove.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/bridge/withdraw-prove.ts b/packages/cli/src/commands/bridge/withdraw-prove.ts index 312ee6bd1c..236b8f549b 100644 --- a/packages/cli/src/commands/bridge/withdraw-prove.ts +++ b/packages/cli/src/commands/bridge/withdraw-prove.ts @@ -93,8 +93,10 @@ export default class BridgeWithdrawProve extends BaseCommand { if (status !== 'ready-to-prove' && status !== 'waiting-to-prove') { const messages: Record = { - 'waiting-to-finalize': 'This withdrawal has already been proven. Wait for the 7-day challenge period to pass, then run bridge:withdraw-finalize.', - 'ready-to-finalize': 'This withdrawal has already been proven and is ready to finalize. Run bridge:withdraw-finalize.', + 'waiting-to-finalize': + 'This withdrawal has already been proven. Wait for the 7-day challenge period to pass, then run bridge:withdraw-finalize.', + 'ready-to-finalize': + 'This withdrawal has already been proven and is ready to finalize. Run bridge:withdraw-finalize.', finalized: 'This withdrawal has already been finalized.', } throw new Error(messages[status] || `Unexpected withdrawal status: ${status}`) @@ -136,7 +138,9 @@ export default class BridgeWithdrawProve extends BaseCommand { console.log('\nWithdrawal proof submitted! Next steps:') console.log(' 1. Wait 7 days for the challenge period to pass') console.log(' 2. Run: celocli bridge:withdraw-status --txHash ' + txHash + ' ...') - console.log(' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...') + console.log( + ' 3. When ready, run: celocli bridge:withdraw-finalize --txHash ' + txHash + ' ...' + ) } else { throw new Error('Prove transaction failed. Please check the transaction on a block explorer.') }