From 2501e281a416cc4912e2240d4894ab02cb7c6ca8 Mon Sep 17 00:00:00 2001 From: danijelTxFusion Date: Tue, 8 Apr 2025 00:00:01 +0200 Subject: [PATCH] feat(zksync): add support for v26, remove support for v25 --- .changeset/breezy-eyes-peel.md | 5 + .../zksync/actions/claimFailedDeposit.md | 157 +++++++++ site/sidebar.ts | 4 + src/zksync/actions/claimFailedDeposit.test.ts | 93 +++++ src/zksync/actions/claimFailedDeposit.ts | 319 ++++++++++++++++++ src/zksync/actions/deposit.ts | 130 ++++++- src/zksync/actions/finalizeWithdrawal.ts | 104 +++--- .../actions/getDefaultBridgeAddresses.test.ts | 2 + .../actions/getDefaultBridgeAddresses.ts | 3 + src/zksync/actions/isWithdrawalFinalized.ts | 26 +- src/zksync/actions/withdraw.ts | 72 +++- src/zksync/constants/address.ts | 9 + src/zksync/decorators/walletL1.test.ts | 26 +- src/zksync/decorators/walletL1.ts | 50 ++- src/zksync/errors/bridge.test.ts | 34 ++ src/zksync/errors/bridge.ts | 35 ++ src/zksync/index.ts | 6 + src/zksync/types/contract.ts | 2 + src/zksync/types/eip1193.ts | 2 + test/src/zksync.ts | 134 +++++++- 20 files changed, 1114 insertions(+), 99 deletions(-) create mode 100644 .changeset/breezy-eyes-peel.md create mode 100644 site/pages/zksync/actions/claimFailedDeposit.md create mode 100644 src/zksync/actions/claimFailedDeposit.test.ts create mode 100644 src/zksync/actions/claimFailedDeposit.ts diff --git a/.changeset/breezy-eyes-peel.md b/.changeset/breezy-eyes-peel.md new file mode 100644 index 0000000000..e906587ca6 --- /dev/null +++ b/.changeset/breezy-eyes-peel.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added support for ZKsync v26, removed support for v25. diff --git a/site/pages/zksync/actions/claimFailedDeposit.md b/site/pages/zksync/actions/claimFailedDeposit.md new file mode 100644 index 0000000000..78d9f87af9 --- /dev/null +++ b/site/pages/zksync/actions/claimFailedDeposit.md @@ -0,0 +1,157 @@ +--- +description: Withdraws funds from the initiated deposit, which failed when finalizing on L2. +--- + +# claimFailedDeposit + +Withdraws funds from the initiated deposit, which failed when finalizing on L2. +If the deposit L2 transaction has failed, it sends an L1 transaction calling `claimFailedDeposit` method of the +L1 bridge, which results in returning L1 tokens back to the depositor. + +## Usage + +:::code-group + +```ts [example.ts] +import { account, walletClient, zksyncClient } from './config' +import { legacyEthAddress } from 'viem/zksync' + +const hash = await walletClient.claimFailedDeposit({ + account, + client: zksyncClient, + depositHash: '' +}) +``` + +```ts [config.ts] +import { createWalletClient, createPublicClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { zksync, mainnet } from 'viem/chains' +import { publicActionsL2, walletActionsL1 } from 'viem/zksync' + +export const zksyncClient = createPublicClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(publicActionsL2()) + +export const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum) +}).extend(walletActionsL1()) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `claimFailedDeposit`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet#account). + +:::code-group + +```ts [example.ts] +import { walletClient, zksyncClient } from './config' +import { legacyEthAddress } from 'viem/zksync' + +const hash = await walletClient.claimFailedDeposit({ + client: zksyncClient, + depositHash: '' +}) +``` + +```ts [config.ts (JSON-RPC Account)] +import { createWalletClient, custom } from 'viem' +import { zksync } from 'viem/chains' +import { publicActionsL2, walletActionsL1 } from 'viem/zksync' + +export const zksyncClient = createPublicClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(publicActionsL2()) + +// Retrieve Account from an EIP-1193 Provider. // [!code focus] +const [account] = await window.ethereum.request({ // [!code focus] + method: 'eth_requestAccounts' // [!code focus] +}) // [!code focus] + +export const walletClient = createWalletClient({ + account, + transport: custom(window.ethereum) // [!code focus] +}).extend(walletActionsL1()) +``` + +```ts [config.ts (Local Account)] +import { createWalletClient, custom } from 'viem' +import { zksync } from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' +import { publicActionsL2, walletActionsL1 } from 'viem/zksync' + +export const zksyncClient = createPublicClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(publicActionsL2()) + +export const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), // [!code focus] + transport: custom(window.ethereum) +}).extend(walletActionsL1()) +``` + +::: + +## Returns + +[`Hash`](/docs/glossary/types#hash) + +The [Transaction](/docs/glossary/terms#transaction) hash. + +## Parameters + +### client + +- **Type:** `Client` + +The L2 client for fetching data from L2 chain. + +```ts +const hash = await walletClient.claimFailedDeposit({ + client: zksyncClient, // [!code focus] + depositHash: '' +}) +``` + +### depositHash + +- **Type:** `Hash` + +The L2 transaction hash of the failed deposit. + +```ts +const hash = await walletClient.claimFailedDeposit({ + client: zksyncClient, + depositHash: '', // [!code focus] +}) +``` + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `walletClient.chain` + +The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown. + +```ts +import { zksync } from 'viem/chains' // [!code focus] + +const hash = await walletClient.claimFailedDeposit({ + chain: zksync, // [!code focus] + client: zksyncClient, + depositHash: '' +}) +``` \ No newline at end of file diff --git a/site/sidebar.ts b/site/sidebar.ts index aaee15e027..bb2300d09d 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -1840,6 +1840,10 @@ export const sidebar = { text: 'deposit', link: '/zksync/actions/deposit', }, + { + text: 'claimFailedDeposit', + link: '/zksync/actions/claimFailedDeposit', + }, ], }, { diff --git a/src/zksync/actions/claimFailedDeposit.test.ts b/src/zksync/actions/claimFailedDeposit.test.ts new file mode 100644 index 0000000000..270d7c7eb7 --- /dev/null +++ b/src/zksync/actions/claimFailedDeposit.test.ts @@ -0,0 +1,93 @@ +import { expect, test } from 'vitest' +import { anvilMainnet, anvilZksync } from '~test/src/anvil.js' +import { accounts } from '~test/src/constants.js' +import { + mockFailedDepositReceipt, + mockFailedDepositTransaction, + mockLogProof, + mockRequestReturnData, +} from '~test/src/zksync.js' +import { privateKeyToAccount } from '~viem/accounts/privateKeyToAccount.js' +import { type EIP1193RequestFn, publicActions } from '~viem/index.js' +import { claimFailedDeposit } from '~viem/zksync/actions/claimFailedDeposit.js' +import { publicActionsL2 } from '~viem/zksync/index.js' + +const request = (async ({ method, params }) => { + if (method === 'eth_sendRawTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + if (method === 'eth_sendTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + if (method === 'eth_estimateGas') return 158774n + if (method === 'eth_gasPrice') return 150_000_000n + if (method === 'eth_maxPriorityFeePerGas') return 100_000_000n + if (method === 'eth_call') + return '0x00000000000000000000000070a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55' + if (method === 'eth_getTransactionCount') return 1n + if (method === 'eth_getBlockByNumber') return anvilMainnet.forkBlockNumber + if (method === 'eth_chainId') return anvilMainnet.chain.id + return anvilMainnet.getClient().request({ method, params } as any) +}) as EIP1193RequestFn + +const baseClient = anvilMainnet.getClient({ batch: { multicall: false } }) +baseClient.request = request +const client = baseClient.extend(publicActions) + +const baseClientWithAccount = anvilMainnet.getClient({ + batch: { multicall: false }, + account: true, +}) +baseClientWithAccount.request = request +const clientWithAccount = baseClientWithAccount.extend(publicActions) + +const baseClientL2 = anvilZksync.getClient() +baseClientL2.request = (async ({ method, params }) => { + if (method === 'eth_getTransactionReceipt') return mockFailedDepositReceipt + if (method === 'eth_getTransactionByHash') return mockFailedDepositTransaction + if (method === 'zks_getL2ToL1LogProof') return mockLogProof + if (method === 'eth_call') + return '0x00000000000000000000000070a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55' + if (method === 'eth_estimateGas') return 158774n + return ( + (await mockRequestReturnData(method)) ?? + (await anvilZksync.getClient().request({ method, params } as any)) + ) +}) as EIP1193RequestFn +const clientL2 = baseClientL2.extend(publicActionsL2()) + +test.skip('default', async () => { + const account = privateKeyToAccount(accounts[0].privateKey) + expect( + claimFailedDeposit(client, { + client: clientL2, + account, + depositHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + }), + ).toBeDefined() +}) + +test('default: account hoisting', async () => { + expect( + claimFailedDeposit(clientWithAccount, { + client: clientL2, + depositHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + }), + ).toBeDefined() +}) + +test('errors: no account provided', async () => { + await expect(() => + claimFailedDeposit(client, { + client: clientL2, + depositHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [AccountNotFoundError: Could not find an Account to execute with this Action. + Please provide an Account with the \`account\` argument on the Action, or by supplying an \`account\` to the Client. + + Docs: https://viem.sh/docs/actions/wallet/sendTransaction + Version: viem@x.y.z] + `) +}) diff --git a/src/zksync/actions/claimFailedDeposit.ts b/src/zksync/actions/claimFailedDeposit.ts new file mode 100644 index 0000000000..ced7f03fd3 --- /dev/null +++ b/src/zksync/actions/claimFailedDeposit.ts @@ -0,0 +1,319 @@ +import { type Address, parseAbi } from 'abitype' +import { ethers } from 'ethers' +import type { Account } from '../../accounts/types.js' +import { getTransaction } from '../../actions/public/getTransaction.js' +import { getTransactionReceipt } from '../../actions/public/getTransactionReceipt.js' +import { readContract } from '../../actions/public/readContract.js' +import { + type SendTransactionErrorType, + type SendTransactionParameters, + type SendTransactionReturnType, + sendTransaction, +} from '../../actions/wallet/sendTransaction.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import { AccountNotFoundError } from '../../errors/account.js' +import { ClientChainNotConfiguredError } from '../../errors/chain.js' +import type { TransactionReceiptNotFoundErrorType } from '../../errors/transaction.js' +import type { GetAccountParameter } from '../../types/account.js' +import type { + Chain, + DeriveChain, + GetChainParameter, +} from '../../types/chain.js' +import type { Hash } from '../../types/misc.js' +import type { Hex } from '../../types/misc.js' +import type { UnionEvaluate, UnionOmit } from '../../types/utils.js' +import { + type FormattedTransactionRequest, + decodeAbiParameters, + decodeFunctionData, + encodeAbiParameters, + encodeFunctionData, + isAddressEqual, + parseAccount, +} from '../../utils/index.js' +import { bootloaderFormalAddress } from '../constants/address.js' +import { + CannotClaimSuccessfulDepositError, + type CannotClaimSuccessfulDepositErrorType, + L2BridgeNotFoundError, + type L2BridgeNotFoundErrorType, + LogProofNotFoundError, + type LogProofNotFoundErrorType, +} from '../errors/bridge.js' +import type { ChainEIP712 } from '../types/chain.js' +import type { BridgeContractAddresses } from '../types/contract.js' +import type { ZksyncTransactionReceipt } from '../types/transaction.js' +import { undoL1ToL2Alias } from '../utils/bridge/undoL1ToL2Alias.js' +import { getDefaultBridgeAddresses } from './getDefaultBridgeAddresses.js' +import { getLogProof } from './getLogProof.js' + +export type ClaimFailedDepositParameters< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined, + accountL2 extends Account | undefined = Account | undefined, + _derivedChain extends Chain | undefined = DeriveChain, +> = UnionEvaluate< + UnionOmit, 'data' | 'to' | 'from'> +> & + Partial> & + Partial> & { + /** L2 client. */ + client: Client + /** The L2 transaction hash of the failed deposit. */ + depositHash: Hash + } + +export type ClaimFailedDepositReturnType = SendTransactionReturnType + +export type ClaimFailedDepositErrorType = + | SendTransactionErrorType + | TransactionReceiptNotFoundErrorType + | CannotClaimSuccessfulDepositErrorType + | LogProofNotFoundErrorType + | L2BridgeNotFoundErrorType + +/** + * Withdraws funds from the initiated deposit, which failed when finalizing on L2. + * If the deposit L2 transaction has failed, it sends an L1 transaction calling `claimFailedDeposit` method of the + * L1 bridge, which results in returning L1 tokens back to the depositor. + * + * @param client - Client to use + * @param parameters - {@link ClaimFailedDepositParameters} + * @returns hash - The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link ClaimFailedDepositReturnType} + * + * @example + * import { createPublicClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zksync, mainnet } from 'viem/chains' + * import { claimFailedDeposit, publicActionsL2 } from 'viem/zksync' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: http(), + * }) + * + * const clientL2 = createPublicClient({ + * chain: zksync, + * transport: http(), + * }).extend(publicActionsL2()) + * + * const account = privateKeyToAccount('0x…') + * + * const hash = await claimFailedDeposit(client, { + * client: clientL2, + * account, + * depositHash: , + * }) + * + * @example Account Hoisting + * import { createPublicClient, createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zksync, mainnet } from 'viem/chains' + * import { publicActionsL2 } from 'viem/zksync' + * + * const walletClient = createWalletClient({ + * chain: mainnet, + * transport: http(), + * account: privateKeyToAccount('0x…'), + * }) + * + * const clientL2 = createPublicClient({ + * chain: zksync, + * transport: http(), + * }).extend(publicActionsL2()) + * + * const hash = await claimFailedDeposit(walletClient, { + * client: clientL2, + * depositHash: , + * }) + */ +export async function claimFailedDeposit< + chain extends Chain | undefined, + account extends Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined, + accountL2 extends Account | undefined = Account | undefined, + _derivedChain extends Chain | undefined = DeriveChain, +>( + client: Client, + parameters: ClaimFailedDepositParameters< + chain, + account, + chainOverride, + chainL2, + accountL2 + >, +): Promise { + const { + account: account_ = client.account, + chain: chain_ = client.chain, + client: l2Client, + depositHash, + ...rest + } = parameters + + const account = account_ ? parseAccount(account_) : client.account + if (!account) + throw new AccountNotFoundError({ + docsPath: '/docs/actions/wallet/sendTransaction', + }) + if (!l2Client.chain) throw new ClientChainNotConfiguredError() + + const receipt = ( + await getTransactionReceipt(l2Client, { hash: depositHash }) + ) + + const successL2ToL1LogIndex = receipt.l2ToL1Logs.findIndex( + (l2ToL1log) => + isAddressEqual(l2ToL1log.sender, bootloaderFormalAddress) && + l2ToL1log.key === depositHash, + ) + const successL2ToL1Log = receipt.l2ToL1Logs[successL2ToL1LogIndex] + if (successL2ToL1Log?.value !== ethers.ZeroHash) + throw new CannotClaimSuccessfulDepositError({ hash: depositHash }) + + const tx = await getTransaction(l2Client, { hash: depositHash }) + + // Undo the aliasing, since the Mailbox contract set it as for contract address. + const l1BridgeAddress = undoL1ToL2Alias(receipt.from) + const l2BridgeAddress = receipt.to + if (!l2BridgeAddress) throw new L2BridgeNotFoundError() + + const l1NativeTokenVault = (await getBridgeAddresses(client, l2Client)) + .l1NativeTokenVault + + let depositSender: Hex + let assetId: Hex + let assetData: Hex + try { + const { args } = decodeFunctionData({ + abi: parseAbi([ + 'function finalizeDeposit(address _l1Sender, address _l2Receiver, address _l1Token, uint256 _amount, bytes _data)', + ]), + data: tx.input, + }) + assetData = encodeAbiParameters( + [{ type: 'uint256' }, { type: 'address' }, { type: 'address' }], + [args[3], args[1], args[2]], + ) + assetId = await readContract(client, { + address: l1NativeTokenVault, + abi: parseAbi(['function assetId(address token) view returns (bytes32)']), + functionName: 'assetId', + args: [args[2]], + }) + depositSender = args[0] + if (assetId === ethers.ZeroHash) + throw new Error(`Token ${args[2]} not registered in NTV`) + } catch (_e) { + const { args } = decodeFunctionData({ + abi: parseAbi([ + 'function finalizeDeposit(uint256 _chainId, bytes32 _assetId, bytes _transferData)', + ]), + data: tx.input, + }) + assetId = args[1] + const transferData = args[2] + const l1TokenAddress = await readContract(client, { + address: l1NativeTokenVault, + abi: parseAbi([ + 'function tokenAddress(bytes32 assetId) view returns (address)', + ]), + functionName: 'tokenAddress', + args: [assetId], + }) + const transferDataDecoded = decodeAbiParameters( + [ + { type: 'address' }, + { type: 'address' }, + { type: 'address' }, + { type: 'uint256' }, + { type: 'bytes' }, + ], + transferData, + ) + assetData = encodeAbiParameters( + [{ type: 'uint256' }, { type: 'address' }, { type: 'address' }], + [transferDataDecoded[3], transferDataDecoded[1], l1TokenAddress], + ) + depositSender = transferDataDecoded[0] + } + + const proof = await getLogProof(l2Client, { + txHash: depositHash, + index: successL2ToL1LogIndex, + }) + if (!proof) + throw new LogProofNotFoundError({ + hash: depositHash, + index: successL2ToL1LogIndex, + }) + + const data = encodeFunctionData({ + abi: parseAbi([ + 'function bridgeRecoverFailedTransfer(uint256 _chainId, address _depositSender, bytes32 _assetId, bytes _assetData, bytes32 _l2TxHash, uint256 _l2BatchNumber, uint256 _l2MessageIndex, uint16 _l2TxNumberInBatch, bytes32[] _merkleProof)', + ]), + functionName: 'bridgeRecoverFailedTransfer', + args: [ + BigInt(l2Client.chain.id), + depositSender, + assetId, + assetData, + depositHash, + receipt.l1BatchNumber!, + BigInt(proof.id), + Number(receipt.l1BatchTxIndex), + proof.proof, + ], + }) + + return await sendTransaction(client, { + chain: chain_, + account, + to: l1BridgeAddress, + data, + ...rest, + } as SendTransactionParameters) +} + +async function getBridgeAddresses< + chain extends Chain | undefined, + chainL2 extends ChainEIP712 | undefined, +>( + client: Client, + l2Client: Client, +): Promise< + BridgeContractAddresses & { + l1Nullifier: Address + l1NativeTokenVault: Address + } +> { + const addresses = await getDefaultBridgeAddresses(l2Client) + let l1Nullifier = addresses.l1Nullifier + let l1NativeTokenVault = addresses.l1NativeTokenVault + + if (!l1Nullifier) + l1Nullifier = await readContract(client, { + address: addresses.sharedL1, + abi: parseAbi(['function L1_NULLIFIER() view returns (address)']), + functionName: 'L1_NULLIFIER', + args: [], + }) + if (!l1NativeTokenVault) + l1NativeTokenVault = await readContract(client, { + address: addresses.sharedL1, + abi: parseAbi(['function nativeTokenVault() view returns (address)']), + functionName: 'nativeTokenVault', + args: [], + }) + + return { + ...addresses, + l1Nullifier, + l1NativeTokenVault, + } +} diff --git a/src/zksync/actions/deposit.ts b/src/zksync/actions/deposit.ts index c77bd42241..3b64f4f460 100644 --- a/src/zksync/actions/deposit.ts +++ b/src/zksync/actions/deposit.ts @@ -1,4 +1,5 @@ -import { type Address, parseAbi, parseAbiParameters } from 'abitype' +import { type Address, parseAbi } from 'abitype' +import { ethers } from 'ethers' import type { Account } from '../../accounts/types.js' import { type EstimateGasParameters, @@ -33,14 +34,17 @@ import type { Hex } from '../../types/misc.js' import type { UnionEvaluate, UnionOmit } from '../../types/utils.js' import { type FormattedTransactionRequest, + concatHex, encodeAbiParameters, encodeFunctionData, isAddressEqual, + keccak256, parseAccount, } from '../../utils/index.js' import { bridgehubAbi } from '../constants/abis.js' import { ethAddressInContracts, + l2NativeTokenVaultAddress, legacyEthAddress, } from '../constants/address.js' import { requiredL1ToL2GasPerPubdataLimit } from '../constants/number.js' @@ -184,7 +188,6 @@ export type DepositErrorType = * * const hash = await deposit(walletClient, { * client: clientL2, - * account, * token: legacyEthAddress, * to: walletClient.account.address, * amount: 1_000_000_000_000_000_000n, @@ -228,7 +231,7 @@ export async function deposit< if (isAddressEqual(token, legacyEthAddress)) token = ethAddressInContracts - const bridgeAddresses = await getDefaultBridgeAddresses(l2Client) + const bridgeAddresses = await getBridgeAddresses(client, l2Client) const bridgehub = await getBridgehubContractAddress(l2Client) const baseToken = await readContract(client, { address: bridgehub, @@ -240,7 +243,6 @@ export async function deposit< const { mintValue, tx } = await getL1DepositTx( client, account, - // @ts-ignore { ...parameters, token }, bridgeAddresses, bridgehub, @@ -296,7 +298,10 @@ async function getL1DepositTx< accountL2, _derivedChain >, - bridgeAddresses: BridgeContractAddresses, + bridgeAddresses: BridgeContractAddresses & { + l1Nullifier: Address + l1NativeTokenVault: Address + }, bridgehub: Address, baseToken: Address, ) { @@ -406,9 +411,12 @@ async function getL1DepositTx< refundRecipient, secondBridgeAddress: bridgeAddress, secondBridgeValue: 0n, - secondBridgeCalldata: encodeAbiParameters( - parseAbiParameters('address x, uint256 y, address z'), - [token, amount, to], + secondBridgeCalldata: await getSecondBridgeCalldata( + client, + bridgeAddresses.l1NativeTokenVault, + token, + amount, + to, ), }, ], @@ -435,9 +443,12 @@ async function getL1DepositTx< refundRecipient, secondBridgeAddress: bridgeAddress, secondBridgeValue: amount, - secondBridgeCalldata: encodeAbiParameters( - parseAbiParameters('address x, uint256 y, address z'), - [ethAddressInContracts, 0n, to], + secondBridgeCalldata: await getSecondBridgeCalldata( + client, + bridgeAddresses.l1NativeTokenVault, + ethAddressInContracts, + amount, + to, ), }, ], @@ -464,9 +475,12 @@ async function getL1DepositTx< refundRecipient, secondBridgeAddress: bridgeAddress, secondBridgeValue: 0n, - secondBridgeCalldata: encodeAbiParameters( - parseAbiParameters('address x, uint256 y, address z'), - [token, amount, to], + secondBridgeCalldata: await getSecondBridgeCalldata( + client, + bridgeAddresses.l1NativeTokenVault, + token, + amount, + to, ), }, ], @@ -839,6 +853,94 @@ async function encodeDefaultBridgeData( ) } +async function getSecondBridgeCalldata( + client: Client, + l1NativeTokenVault: Address, + token: Address, + amount: bigint, + to: Address, +): Promise { + let assetId: Hex + let token_ = token + if (isAddressEqual(token, legacyEthAddress)) token_ = ethAddressInContracts + + const assetIdFromNTV = await readContract(client, { + address: l1NativeTokenVault, + abi: parseAbi(['function assetId(address token) view returns (bytes32)']), + functionName: 'assetId', + args: [token_], + }) + + if (assetIdFromNTV && assetIdFromNTV !== ethers.ZeroHash) + assetId = assetIdFromNTV + else { + // Okay, the token have not been registered within the Native token vault. + // There are two cases when it is possible: + // - The token is native to L1 (it may or may not be bridged), but it has not been + // registered within NTV after the Gateway upgrade. We assume that this is not the case + // as the SDK is expected to work only after the full migration is done. + // - The token is native to the current chain and it has never been bridged. + + if (!client.chain) throw new ClientChainNotConfiguredError() + assetId = keccak256( + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'address' }, { type: 'address' }], + [BigInt(client.chain.id), l2NativeTokenVaultAddress, token_], + ), + ) + } + + const ntvData = encodeAbiParameters( + [{ type: 'uint256' }, { type: 'address' }, { type: 'address' }], + [BigInt(amount), to, token_], + ) + + const data = encodeAbiParameters( + [{ type: 'bytes32' }, { type: 'bytes' }], + [assetId, ntvData], + ) + + return concatHex(['0x01', data]) +} + +async function getBridgeAddresses< + chain extends Chain | undefined, + chainL2 extends ChainEIP712 | undefined, +>( + client: Client, + l2Client: Client, +): Promise< + BridgeContractAddresses & { + l1Nullifier: Address + l1NativeTokenVault: Address + } +> { + const addresses = await getDefaultBridgeAddresses(l2Client) + let l1Nullifier = addresses.l1Nullifier + let l1NativeTokenVault = addresses.l1NativeTokenVault + + if (!l1Nullifier) + l1Nullifier = await readContract(client, { + address: addresses.sharedL1, + abi: parseAbi(['function L1_NULLIFIER() view returns (address)']), + functionName: 'L1_NULLIFIER', + args: [], + }) + if (!l1NativeTokenVault) + l1NativeTokenVault = await readContract(client, { + address: addresses.sharedL1, + abi: parseAbi(['function nativeTokenVault() view returns (address)']), + functionName: 'nativeTokenVault', + args: [], + }) + + return { + ...addresses, + l1Nullifier, + l1NativeTokenVault, + } +} + function scaleGasLimit(gasLimit: bigint): bigint { return (gasLimit * BigInt(12)) / BigInt(10) } diff --git a/src/zksync/actions/finalizeWithdrawal.ts b/src/zksync/actions/finalizeWithdrawal.ts index 59a4345665..b53ec5d0cd 100644 --- a/src/zksync/actions/finalizeWithdrawal.ts +++ b/src/zksync/actions/finalizeWithdrawal.ts @@ -1,4 +1,4 @@ -import type { Address } from 'abitype' +import { type Address, parseAbi } from 'abitype' import type { Account } from '../../accounts/types.js' import { readContract } from '../../actions/public/readContract.js' import { @@ -20,17 +20,15 @@ import type { Hex } from '../../types/misc.js' import { decodeAbiParameters, encodeFunctionData, - isAddressEqual, parseAccount, slice, } from '../../utils/index.js' -import { l1SharedBridgeAbi, l2SharedBridgeAbi } from '../constants/abis.js' -import { l2BaseTokenAddress } from '../constants/address.js' import { WithdrawalLogNotFoundError, type WithdrawalLogNotFoundErrorType, } from '../errors/bridge.js' import type { ChainEIP712 } from '../types/chain.js' +import type { BridgeContractAddresses } from '../types/contract.js' import { getWithdrawalL2ToL1Log } from '../utils/bridge/getWithdrawalL2ToL1Log.js' import { getWithdrawalLog } from '../utils/bridge/getWithdrawalLog.js' import { getDefaultBridgeAddresses } from './getDefaultBridgeAddresses.js' @@ -149,52 +147,35 @@ export async function finalizeWithdrawal< }) if (!l2Client.chain) throw new ChainNotFoundError() - const { - l1BatchNumber, - l2MessageIndex, - l2TxNumberInBlock, - message, - sender, - proof, - } = await getFinalizeWithdrawalParams(l2Client, { hash, index }) - - let l1Bridge: Address + const finalizeWithdrawalParams = await getFinalizeWithdrawalParams(l2Client, { + hash, + index, + }) - if (isAddressEqual(sender, l2BaseTokenAddress)) - l1Bridge = (await getDefaultBridgeAddresses(l2Client)).sharedL1 - else if (!(await isLegacyBridge(l2Client, { address: sender }))) - l1Bridge = await readContract(l2Client, { - address: sender, - abi: l2SharedBridgeAbi, - functionName: 'l1SharedBridge', - args: [], - }) - else - l1Bridge = await readContract(l2Client, { - address: sender, - abi: l2SharedBridgeAbi, - functionName: 'l1Bridge', - args: [], - }) + const l1Nullifier = (await getBridgeAddresses(client, l2Client)).l1Nullifier const data = encodeFunctionData({ - abi: l1SharedBridgeAbi, - functionName: 'finalizeWithdrawal', + abi: parseAbi([ + 'function finalizeDeposit((uint256 chainId, uint256 l2BatchNumber, uint256 l2MessageIndex, address l2Sender, uint16 l2TxNumberInBatch, bytes message, bytes32[] merkleProof) _finalizeWithdrawalParams)', + ]), + functionName: 'finalizeDeposit', args: [ - BigInt(l2Client.chain.id), - l1BatchNumber!, - BigInt(l2MessageIndex), - Number(l2TxNumberInBlock), - message, - proof, + { + chainId: BigInt(l2Client.chain.id), + l2BatchNumber: finalizeWithdrawalParams.l1BatchNumber!, + l2MessageIndex: BigInt(finalizeWithdrawalParams.l2MessageIndex), + l2Sender: finalizeWithdrawalParams.sender, + l2TxNumberInBatch: Number(finalizeWithdrawalParams.l2TxNumberInBlock), + message: finalizeWithdrawalParams.message, + merkleProof: finalizeWithdrawalParams.proof, + }, ], }) return await sendTransaction(client, { account, - to: l1Bridge, + to: l1Nullifier, data, - value: 0n, ...rest, } as SendTransactionParameters) } @@ -230,19 +211,40 @@ async function getFinalizeWithdrawalParams< } } -async function isLegacyBridge< +async function getBridgeAddresses< chain extends Chain | undefined, - account extends Account | undefined, ->(client: Client, parameters: { address: Address }) { - try { - await readContract(client, { - address: parameters.address, - abi: l2SharedBridgeAbi, - functionName: 'l1SharedBridge', + chainL2 extends ChainEIP712 | undefined, +>( + client: Client, + l2Client: Client, +): Promise< + BridgeContractAddresses & { + l1Nullifier: Address + l1NativeTokenVault: Address + } +> { + const addresses = await getDefaultBridgeAddresses(l2Client) + let l1Nullifier = addresses.l1Nullifier + let l1NativeTokenVault = addresses.l1NativeTokenVault + + if (!l1Nullifier) + l1Nullifier = await readContract(client, { + address: addresses.sharedL1, + abi: parseAbi(['function L1_NULLIFIER() view returns (address)']), + functionName: 'L1_NULLIFIER', args: [], }) - return false - } catch (_e) { - return true + if (!l1NativeTokenVault) + l1NativeTokenVault = await readContract(client, { + address: addresses.sharedL1, + abi: parseAbi(['function nativeTokenVault() view returns (address)']), + functionName: 'nativeTokenVault', + args: [], + }) + + return { + ...addresses, + l1Nullifier, + l1NativeTokenVault, } } diff --git a/src/zksync/actions/getDefaultBridgeAddresses.test.ts b/src/zksync/actions/getDefaultBridgeAddresses.test.ts index 49c1cca5e3..19b6bfe931 100644 --- a/src/zksync/actions/getDefaultBridgeAddresses.test.ts +++ b/src/zksync/actions/getDefaultBridgeAddresses.test.ts @@ -14,6 +14,8 @@ test('default', async () => { expect(addresses).toMatchInlineSnapshot(` { "erc20L1": "0xbe270c78209cfda84310230aaa82e18936310b2e", + "l1NativeTokenVault": "0xeC1D6d4A357bd65226eBa599812ba4fDA5514F47", + "l1Nullifier": "0xFb2fdA7D9377F98a6cbD7A61C9f69575c8E947b6", "sharedL1": "0x648afeaf09a3db988ac41b786001235bbdbc7640", "sharedL2": "0xfd61c893b903fa133908ce83dfef67c4c2350dd8", } diff --git a/src/zksync/actions/getDefaultBridgeAddresses.ts b/src/zksync/actions/getDefaultBridgeAddresses.ts index 7ec25e2b9b..6d6c88306b 100644 --- a/src/zksync/actions/getDefaultBridgeAddresses.ts +++ b/src/zksync/actions/getDefaultBridgeAddresses.ts @@ -14,9 +14,12 @@ export async function getDefaultBridgeAddresses< client: Client, ): Promise { const addresses = await client.request({ method: 'zks_getBridgeContracts' }) + return { erc20L1: addresses.l1Erc20DefaultBridge, sharedL1: addresses.l1SharedDefaultBridge, sharedL2: addresses.l2SharedDefaultBridge, + l1Nullifier: addresses.l1Nullifier, + l1NativeTokenVault: addresses.l1NativeTokenVault, } } diff --git a/src/zksync/actions/isWithdrawalFinalized.ts b/src/zksync/actions/isWithdrawalFinalized.ts index 533a98dd14..f7ec03e502 100644 --- a/src/zksync/actions/isWithdrawalFinalized.ts +++ b/src/zksync/actions/isWithdrawalFinalized.ts @@ -1,4 +1,3 @@ -import type { Address } from 'abitype' import type { Account } from '../../accounts/types.js' import { readContract } from '../../actions/public/readContract.js' import type { Client } from '../../clients/createClient.js' @@ -9,9 +8,7 @@ import { } from '../../errors/chain.js' import type { Chain } from '../../types/chain.js' import type { Hash } from '../../types/misc.js' -import { isAddressEqual, slice } from '../../utils/index.js' -import { l1SharedBridgeAbi, l2SharedBridgeAbi } from '../constants/abis.js' -import { l2BaseTokenAddress } from '../constants/address.js' +import { l1SharedBridgeAbi } from '../constants/abis.js' import { WithdrawalLogNotFoundError, type WithdrawalLogNotFoundErrorType, @@ -19,7 +16,6 @@ import { import type { ChainEIP712 } from '../types/chain.js' import { getWithdrawalL2ToL1Log } from '../utils/bridge/getWithdrawalL2ToL1Log.js' import { getWithdrawalLog } from '../utils/bridge/getWithdrawalLog.js' -import { getBaseTokenL1Address } from './getBaseTokenL1Address.js' import { getDefaultBridgeAddresses } from './getDefaultBridgeAddresses.js' import { getLogProof } from './getLogProof.js' @@ -87,7 +83,7 @@ export async function isWithdrawalFinalized< hash, index, }) - const sender = slice(log.topics[1]!, 12) as Address + // `getLogProof` is called not to get proof but // to get the index of the corresponding L2->L1 log, // which is returned as `proof.id`. @@ -95,23 +91,9 @@ export async function isWithdrawalFinalized< txHash: hash, index: l2ToL1LogIndex!, }) - if (!proof) { - throw new WithdrawalLogNotFoundError({ hash }) - } + if (!proof) throw new WithdrawalLogNotFoundError({ hash }) - let l1Bridge: Address - if ( - isAddressEqual(sender, await getBaseTokenL1Address(l2Client)) || - isAddressEqual(sender, l2BaseTokenAddress) - ) - l1Bridge = (await getDefaultBridgeAddresses(l2Client)).sharedL1 - else - l1Bridge = await readContract(l2Client, { - address: sender, - abi: l2SharedBridgeAbi, - functionName: 'l1SharedBridge', - args: [], - }) + const l1Bridge = (await getDefaultBridgeAddresses(l2Client)).sharedL1 return await readContract(client, { address: l1Bridge, diff --git a/src/zksync/actions/withdraw.ts b/src/zksync/actions/withdraw.ts index 660bdbe669..866de8aca2 100644 --- a/src/zksync/actions/withdraw.ts +++ b/src/zksync/actions/withdraw.ts @@ -1,26 +1,33 @@ -import type { Address } from 'abitype' +import { type Address, parseAbi } from 'abitype' import type { Account } from '../../accounts/types.js' +import { readContract } from '../../actions/public/readContract.js' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../errors/account.js' +import { ClientChainNotConfiguredError } from '../../errors/chain.js' import type { GetAccountParameter } from '../../types/account.js' import type { GetChainParameter } from '../../types/chain.js' import type { UnionOmit } from '../../types/utils.js' import type { EncodeFunctionDataReturnType } from '../../utils/abi/encodeFunctionData.js' import { + encodeAbiParameters, encodeFunctionData, isAddressEqual, + keccak256, parseAccount, } from '../../utils/index.js' import { ethTokenAbi, l2SharedBridgeAbi } from '../constants/abis.js' import { ethAddressInContracts, + l2AssetRouterAddress, l2BaseTokenAddress, + l2NativeTokenVaultAddress, legacyEthAddress, } from '../constants/address.js' import type { ChainEIP712 } from '../types/chain.js' import type { ZksyncTransactionRequest } from '../types/transaction.js' import { getDefaultBridgeAddresses } from './getDefaultBridgeAddresses.js' +import { getL1ChainId } from './getL1ChainId.js' import { getL2TokenAddress } from './getL2TokenAddress.js' import { type SendTransactionErrorType, @@ -133,6 +140,7 @@ export async function withdraw< ): Promise { let { account: account_ = client.account, + chain: chain_ = client.chain, token = l2BaseTokenAddress, to, amount, @@ -165,17 +173,65 @@ export async function withdraw< value = amount contract = l2BaseTokenAddress } else { - data = encodeFunctionData({ - abi: l2SharedBridgeAbi, - functionName: 'withdraw', - args: [to, token, amount], + const assetId = await readContract(client, { + address: l2NativeTokenVaultAddress, + abi: parseAbi(['function assetId(address token) view returns (bytes32)']), + functionName: 'assetId', + args: [token], }) - contract = bridgeAddress - ? bridgeAddress - : (await getDefaultBridgeAddresses(client)).sharedL2 + const originChainId = await readContract(client, { + address: l2NativeTokenVaultAddress, + abi: parseAbi([ + 'function originChainId(bytes32 assetId) view returns (uint256)', + ]), + functionName: 'originChainId', + args: [assetId], + }) + const l1ChainId = await getL1ChainId(client) + + const isTokenL1Native = + originChainId === BigInt(l1ChainId) || token === ethAddressInContracts + if (!bridgeAddress) { + // If the legacy L2SharedBridge is deployed we use it for l1 native tokens. + bridgeAddress = isTokenL1Native + ? (await getDefaultBridgeAddresses(client)).sharedL2 + : l2AssetRouterAddress + } + // For non L1 native tokens we need to use the AssetRouter. + // For L1 native tokens we can use the legacy withdraw method. + if (!isTokenL1Native) { + contract = l2AssetRouterAddress + if (!chain_) throw new ClientChainNotConfiguredError() + const chainId = chain_.id + const assetId = keccak256( + encodeAbiParameters( + [{ type: 'uint256' }, { type: 'address' }, { type: 'address' }], + [BigInt(chainId), l2NativeTokenVaultAddress, token], + ), + ) + const assetData = encodeAbiParameters( + [{ type: 'uint256' }, { type: 'address' }, { type: 'address' }], + [BigInt(amount), to, token], + ) + data = encodeFunctionData({ + abi: parseAbi([ + 'function withdraw(bytes32 _assetId, bytes _transferData)', + ]), + functionName: 'withdraw', + args: [assetId, assetData], + }) + } else { + contract = bridgeAddress + data = encodeFunctionData({ + abi: l2SharedBridgeAbi, + functionName: 'withdraw', + args: [to, token, amount], + }) + } } return await sendTransaction(client, { + chain: chain_, account, to: contract, data, diff --git a/src/zksync/constants/address.ts b/src/zksync/constants/address.ts index 3e5f3bb7e8..173e1544ea 100644 --- a/src/zksync/constants/address.ts +++ b/src/zksync/constants/address.ts @@ -21,4 +21,13 @@ export const l1MessengerAddress = export const l1ToL2AliasOffset = '0x1111000000000000000000000000000000001111' as const +export const l2AssetRouterAddress = + '0x0000000000000000000000000000000000010003' as const + +export const l2NativeTokenVaultAddress = + '0x0000000000000000000000000000000000010004' as const + +export const bootloaderFormalAddress = + '0x0000000000000000000000000000000000008001' as const + export const addressModulo = 2n ** 160n diff --git a/src/zksync/decorators/walletL1.test.ts b/src/zksync/decorators/walletL1.test.ts index f967aeac4d..caf3ab139d 100644 --- a/src/zksync/decorators/walletL1.test.ts +++ b/src/zksync/decorators/walletL1.test.ts @@ -1,6 +1,12 @@ import { expect, test } from 'vitest' import { anvilMainnet, anvilZksync } from '~test/src/anvil.js' -import { accounts, mockRequestReturnData } from '~test/src/zksync.js' +import { + accounts, + mockFailedDepositReceipt, + mockFailedDepositTransaction, + mockLogProof, + mockRequestReturnData, +} from '~test/src/zksync.js' import { privateKeyToAccount } from '~viem/accounts/privateKeyToAccount.js' import type { EIP1193RequestFn } from '~viem/index.js' import { @@ -30,6 +36,14 @@ const client = baseClient.extend(walletActionsL1()) const baseZksyncClient = anvilZksync.getClient() baseZksyncClient.request = (async ({ method, params }) => { + if ( + method === 'eth_getTransactionReceipt' && + (params)[0] === + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc' + ) + return mockFailedDepositReceipt + if (method === 'eth_getTransactionByHash') return mockFailedDepositTransaction + if (method === 'zks_getL2ToL1LogProof') return mockLogProof if (method === 'eth_call') return '0x00000000000000000000000070a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55' if (method === 'eth_estimateGas') return 158774n @@ -75,3 +89,13 @@ test('deposit', async () => { }), ).toBeDefined() }) + +test('claimFailedDeposit', async () => { + expect( + await client.claimFailedDeposit({ + client: zksyncClient, + depositHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + }), + ).toBeDefined() +}) diff --git a/src/zksync/decorators/walletL1.ts b/src/zksync/decorators/walletL1.ts index 3125685c42..1a0517f602 100644 --- a/src/zksync/decorators/walletL1.ts +++ b/src/zksync/decorators/walletL1.ts @@ -2,6 +2,11 @@ import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import type { Account } from '../../types/account.js' import type { Chain } from '../../types/chain.js' +import { + type ClaimFailedDepositParameters, + type ClaimFailedDepositReturnType, + claimFailedDeposit, +} from '../actions/claimFailedDeposit.js' import { type DepositParameters, type DepositReturnType, @@ -23,6 +28,49 @@ export type WalletActionsL1< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = { + /** + * Withdraws funds from the initiated deposit, which failed when finalizing on L2. + * If the deposit L2 transaction has failed, it sends an L1 transaction calling `claimFailedDeposit` method of the + * L1 bridge, which results in returning L1 tokens back to the depositor. + * + * @param parameters - {@link ClaimFailedDepositParameters} + * @returns hash - The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link ClaimFailedDepositReturnType} + * + * @example + * import { createPublicClient, createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zksync, mainnet } from 'viem/chains' + * import { walletActionsL1, publicActionsL2 } from 'viem/zksync' + * + * const walletClient = createWalletClient({ + * chain: mainnet, + * transport: http(), + * account: privateKeyToAccount('0x…'), + * }).extend(walletActionsL1()) + * + * const clientL2 = createPublicClient({ + * chain: zksync, + * transport: http(), + * }).extend(publicActionsL2()) + * + * const hash = await walletClient.claimFailedDeposit({ + * client: clientL2, + * depositHash: , + * }) + */ + claimFailedDeposit: < + chainOverride extends Chain | undefined = undefined, + chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined, + accountL2 extends Account | undefined = Account | undefined, + >( + parameters: ClaimFailedDepositParameters< + chain, + account, + chainOverride, + chainL2, + accountL2 + >, + ) => Promise /** * Transfers the specified token from the associated account on the L1 network to the target account on the L2 network. * The token can be either ETH or any ERC20 token. For ERC20 tokens, enough approved tokens must be associated with @@ -53,7 +101,6 @@ export type WalletActionsL1< * * const hash = await walletClient.deposit({ * client: clientL2, - * account, * token: legacyEthAddress, * to: walletClient.account.address, * amount: 1_000_000_000_000_000_000n, @@ -168,6 +215,7 @@ export function walletActionsL1() { >( client: Client, ): WalletActionsL1 => ({ + claimFailedDeposit: (args) => claimFailedDeposit(client, args), deposit: (args) => deposit(client, args), finalizeWithdrawal: (args) => finalizeWithdrawal(client, args), requestExecute: (args) => requestExecute(client, args), diff --git a/src/zksync/errors/bridge.test.ts b/src/zksync/errors/bridge.test.ts index f22575084d..cffddf33a1 100644 --- a/src/zksync/errors/bridge.test.ts +++ b/src/zksync/errors/bridge.test.ts @@ -1,6 +1,9 @@ import { expect, test } from 'vitest' import { BaseFeeHigherThanValueError, + CannotClaimSuccessfulDepositError, + L2BridgeNotFoundError, + LogProofNotFoundError, TxHashNotFoundInLogsError, WithdrawalLogNotFoundError, } from '~viem/zksync/errors/bridge.js' @@ -35,3 +38,34 @@ test('WithdrawalLogNotFoundError', () => { Version: viem@x.y.z] `) }) + +test('CannotClaimSuccessfulDepositError', () => { + const hash = + '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + expect( + new CannotClaimSuccessfulDepositError({ hash }), + ).toMatchInlineSnapshot(` + [CannotClaimSuccessfulDepositError: Cannot claim successful deposit: ${hash}. + + Version: viem@x.y.z] + `) +}) + +test('LogProofNotFoundError', () => { + const hash = + '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + const index = 0 + expect(new LogProofNotFoundError({ hash, index })).toMatchInlineSnapshot(` + [LogProofNotFoundError: Log proof not found for hash ${hash} and index ${index}. + + Version: viem@x.y.z] + `) +}) + +test('L2BridgeNotFoundError', () => { + expect(new L2BridgeNotFoundError()).toMatchInlineSnapshot(` + [L2BridgeNotFoundError: L2 bridge address not found. + + Version: viem@x.y.z] + `) +}) diff --git a/src/zksync/errors/bridge.ts b/src/zksync/errors/bridge.ts index 89082b5fe0..bf997ac22f 100644 --- a/src/zksync/errors/bridge.ts +++ b/src/zksync/errors/bridge.ts @@ -44,3 +44,38 @@ export class WithdrawalLogNotFoundError extends BaseError { ) } } + +export type CannotClaimSuccessfulDepositErrorType = + CannotClaimSuccessfulDepositError & { + name: 'CannotClaimSuccessfulDepositError' + } +export class CannotClaimSuccessfulDepositError extends BaseError { + constructor({ hash }: { hash: Hash }) { + super([`Cannot claim successful deposit: ${hash}.`].join('\n'), { + name: 'CannotClaimSuccessfulDepositError', + }) + } +} + +export type LogProofNotFoundErrorType = LogProofNotFoundError & { + name: 'LogProofNotFoundError' +} +export class LogProofNotFoundError extends BaseError { + constructor({ hash, index }: { hash: Hash; index: number }) { + super( + [`Log proof not found for hash ${hash} and index ${index}.`].join('\n'), + { name: 'LogProofNotFoundError' }, + ) + } +} + +export type L2BridgeNotFoundErrorType = L2BridgeNotFoundError & { + name: 'L2BridgeNotFoundError' +} +export class L2BridgeNotFoundError extends BaseError { + constructor() { + super(['L2 bridge address not found.'].join('\n'), { + name: 'L2BridgeNotFoundError', + }) + } +} diff --git a/src/zksync/index.ts b/src/zksync/index.ts index 2f06c6d99d..6b58c19f10 100644 --- a/src/zksync/index.ts +++ b/src/zksync/index.ts @@ -28,6 +28,12 @@ export { type HashBytecodeErrorType, hashBytecode, } from './utils/hashBytecode.js' +export { + type ClaimFailedDepositErrorType, + type ClaimFailedDepositParameters, + type ClaimFailedDepositReturnType, + claimFailedDeposit, +} from './actions/claimFailedDeposit.js' export { type DepositErrorType, type DepositReturnType, diff --git a/src/zksync/types/contract.ts b/src/zksync/types/contract.ts index 15062cfbdc..c212cc4421 100644 --- a/src/zksync/types/contract.ts +++ b/src/zksync/types/contract.ts @@ -4,6 +4,8 @@ export type BridgeContractAddresses = { erc20L1: Address sharedL1: Address sharedL2: Address + l1Nullifier: Address | undefined + l1NativeTokenVault: Address | undefined } export type ContractDeploymentType = diff --git a/src/zksync/types/eip1193.ts b/src/zksync/types/eip1193.ts index 628a4d5fbd..9e452c2abd 100644 --- a/src/zksync/types/eip1193.ts +++ b/src/zksync/types/eip1193.ts @@ -91,6 +91,8 @@ export type PublicZksyncRpcSchema = [ l2WethBridge: Address l1SharedDefaultBridge: Address l2SharedDefaultBridge: Address + l1Nullifier?: Address + l1NativeTokenVault?: Address } }, { diff --git a/test/src/zksync.ts b/test/src/zksync.ts index 0b5cc0d374..bfaf1408db 100644 --- a/test/src/zksync.ts +++ b/test/src/zksync.ts @@ -72,6 +72,8 @@ export const mockAddresses = { l2Erc20DefaultBridge: '0xfc073319977e314f251eae6ae6be76b0b3baeecf', l1WethBridge: '0x5e6d086f5ec079adff4fb3774cdf3e8d6a34f7e9', l2WethBridge: '0x5e6d086f5ec079adff4fb3774cdf3e8d6a34f7e9', + l1Nullifier: '0xFb2fdA7D9377F98a6cbD7A61C9f69575c8E947b6', + l1NativeTokenVault: '0xeC1D6d4A357bd65226eBa599812ba4fDA5514F47', } export const mockRange = [0, 5] @@ -185,7 +187,7 @@ export const mockTransactionDetails = { export const mockedGasEstimation = 123456789n -const mockedReceipt: ZksyncTransactionReceipt = { +export const mockReceipt: ZksyncTransactionReceipt = { transactionHash: '0x15c295874fe9ad8f6708def4208119c68999f7a76ac6447c111e658ba6bfaa1e', transactionIndex: 0, @@ -365,6 +367,133 @@ const mockedReceipt: ZksyncTransactionReceipt = { effectiveGasPrice: 100000000n, } +export const mockFailedDepositReceipt: ZksyncTransactionReceipt = { + transactionHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + transactionIndex: 0, + blockHash: + '0x1f33506c39562bb7909730a5d3249c46654b49ed5ef9d6561d40ee6dba4c0df3', + blockNumber: 51n, + l1BatchTxIndex: 0n, + l1BatchNumber: 26n, + from: '0x435dd24aa6b5e4cb393168aeadefe782d2ac1f34', + to: '0x0000000000000000000000000000000000010003', + cumulativeGasUsed: 0n, + gasUsed: 300000n, + contractAddress: null, + logs: [ + { + address: '0x000000000000000000000000000000000000800a', + topics: [ + '0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885', + '0x0000000000000000000000000000000000000000000000000000000000008001', + ], + data: '0x0000000000000000000000000000000000000000000000000000479f69c6ac00', + blockHash: + '0x1f33506c39562bb7909730a5d3249c46654b49ed5ef9d6561d40ee6dba4c0df3', + blockNumber: 51n, + l1BatchNumber: 26n, + transactionHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + transactionIndex: 0, + logIndex: 0, + transactionLogIndex: 0, + logType: null, + removed: false, + }, + { + address: '0x0000000000000000000000000000000000008008', + topics: [ + '0x27fe8c0b49f49507b9d4fe5968c9f49edfe5c9df277d433a07a0717ede97638d', + ], + data: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080015b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc0000000000000000000000000000000000000000000000000000000000000000', + blockHash: + '0x1f33506c39562bb7909730a5d3249c46654b49ed5ef9d6561d40ee6dba4c0df3', + blockNumber: 51n, + l1BatchNumber: 26n, + transactionHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + transactionIndex: 0, + logIndex: 1, + transactionLogIndex: 1, + logType: null, + removed: false, + }, + ], + l2ToL1Logs: [ + { + blockNumber: 5907n, + blockHash: + '0x1f33506c39562bb7909730a5d3249c46654b49ed5ef9d6561d40ee6dba4c0df3', + l1BatchNumber: 26n, + transactionIndex: 0n, + shardId: 0n, + isService: true, + sender: '0x0000000000000000000000000000000000008001', + key: '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + value: + '0x0000000000000000000000000000000000000000000000000000000000000000', + transactionHash: + '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + logIndex: 0n, + }, + ], + status: 'reverted', + logsBloom: + '0x00000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020040000000000000040000000000000000000000000000000080000000000000000000000000000000000000000000000400000000000000000000000001000000000000004000100000000000000000000000000000000000080000080000000100000000000000000000000000000000000000000000000000000000000008000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000', + type: '0xff', + effectiveGasPrice: 262500000n, +} + +export const mockFailedDepositTransaction = { + hash: '0x5b08ec4c7ebb02c07a3f08bc5677aec87c47200f685f6389969a3c084bee13dc', + nonce: 0, + blockHash: + '0x1f33506c39562bb7909730a5d3249c46654b49ed5ef9d6561d40ee6dba4c0df3', + blockNumber: 51n, + transactionIndex: 0, + from: '0x435dd24aa6b5e4cb393168aeadefe782d2ac1f34', + to: '0x0000000000000000000000000000000000010003', + value: 0n, + gasPrice: 262500000n, + gas: 300000n, + input: + '0xcfe7af7c00000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc04900000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049000000000000000000000000326437271e12059c2f323922e2c7610d7234941a000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c1010000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003444149000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000344414900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000', + type: 'priority', + maxFeePerGas: 262500000n, + maxPriorityFeePerGas: 0n, + chainId: 271, + l1BatchNumber: 26n, + l1BatchTxIndex: 0n, + maxFeePerBlobGas: undefined, + typeHex: '0xff', + v: undefined, + yParity: undefined, +} + +export const mockLogProof = { + proof: [ + '0x010f000100000000000000000000000000000000000000000000000000000000', + '0x72abee45b59e344af8a6e520241c4744aff26ed411f4c4b00f8af09adada43ba', + '0xc3d03eebfd83049991ea3d3e358b6712e7aa2e2e63dc2d4b438987cec28ac8d0', + '0xe3697c7f33c31a9b0f0aeb8542287d0d21e8c4cf82163d0c44c7a98aa11aa111', + '0x199cc5812543ddceeddd0fc82807646a4899444240db2c0d2f20c3cceb5f51fa', + '0xe4733f281f18ba3ea8775dd62d2fcd84011c8c938f16ea5790fd29a03bf8db89', + '0x1798a1fd9c8fbb818c98cff190daa7cc10b6e5ac9716b4a2649f7c2ebcef2272', + '0x66d7c5983afe44cf15ea8cf565b34c6c31ff0cb4dd744524f7842b942d08770d', + '0xb04e5ee349086985f74b73971ce9dfe76bbed95c84906c5dffd96504e1e5396c', + '0xac506ecb5465659b3a927143f6d724f91d8d9c4bdb2463aee111d9aa869874db', + '0x124b05ec272cecd7538fdafe53b6628d31188ffb6f345139aac3c3c1fd2e470f', + '0xc3be9cbd19304d84cca3d045e06b8db3acd68c304fc9cd4cbffe6d18036cb13f', + '0xfef7bd9f889811e59e4076a0174087135f080177302763019adaf531257e3a87', + '0xa707d1c62d8be699d34cb74804fdd7b4c568b6c1a821066f126c680d4b83e00b', + '0xf6e093070e0389d2e529d60fadb855fdded54976ec50ac709e3a36ceaa64c291', + '0xe4ed1ec13a28c40715db6399f6f99ce04e5f19d60ad3ff6831f098cb6cf75944', + ], + id: 0, + root: '0x16a369954a147966c849e4a55e1762dc4c235cd454f3d9c2e0f4fb32e93c1546', +} + export const mockRequestReturnData = async (method: string) => { if (method === 'zks_L1ChainId') return mockChainId if (method === 'zks_estimateFee') return mockFeeValues @@ -382,7 +511,8 @@ export const mockRequestReturnData = async (method: string) => { if (method === 'zks_getTransactionDetails') return mockTransactionDetails if (method === 'zks_L1BatchNumber') return mockedL1BatchNumber if (method === 'zks_estimateGasL1ToL2') return mockedGasEstimation - if (method === 'eth_getTransactionReceipt') return mockedReceipt + if (method === 'eth_getTransactionReceipt') return mockReceipt + if (method === 'zks_getL2ToL1LogProof') return mockLogProof return undefined }