diff --git a/src/experimental/arbitrumDeposit/actions.ts b/src/experimental/arbitrumDeposit/actions.ts index d87d3cfc8..52556982c 100644 --- a/src/experimental/arbitrumDeposit/actions.ts +++ b/src/experimental/arbitrumDeposit/actions.ts @@ -1,90 +1,77 @@ -import { - type PublicClient, - type WalletClient, - type Client, - encodeFunctionData, - Account, - Address, - parseTransaction, - serializeTransaction +import { BigNumber } from 'ethers' +import { + Account, + Address, + Client, + type PublicClient, + TransactionRequest, } from 'viem' -import { localEthChain } from '../chains' -import { inboxAbi } from './abis/inbox' +import { EthBridger } from '../../lib/assetBridger/ethBridger' +import { transformPublicClientToProvider } from './transformViemToEthers' -export type ArbitrumDepositActions = { - depositEth: (args: { - amount: bigint; - account: Account | Address; - walletClient: WalletClient; - }) => Promise<`0x${string}`> +export type PrepareDepositEthParameters = { + amount: bigint + account: Account | Address } -type ArbitrumChainConfig = { - ethBridge: { - inbox: `0x${string}` - } +export type PrepareDepositEthToParameters = PrepareDepositEthParameters & { + destinationAddress: Address + parentPublicClient: PublicClient } -export function arbitrumDepositActions(childChain: ArbitrumChainConfig) { - return (parentPublicClient: TClient): ArbitrumDepositActions => { - const getDepositRequest = async ({ - amount, - account - }: { - amount: bigint - account: Account | Address - }) => { - const from = typeof account === 'string' ? account : account.address - - return { - to: childChain.ethBridge.inbox, - value: amount, - data: encodeFunctionData({ - abi: inboxAbi, - functionName: 'depositEth', - args: [] - }), - from - } - } +export async function prepareDepositEthTransaction( + client: Client, + { amount, account }: PrepareDepositEthParameters +): Promise { + const provider = transformPublicClientToProvider(client) + const ethBridger = await EthBridger.fromProvider(provider) + const request = await ethBridger.getDepositRequest({ + amount: BigNumber.from(amount), + from: typeof account === 'string' ? account : account.address, + }) - return { - async depositEth({ amount, account, walletClient }) { - const request = await getDepositRequest({ - amount, - account - }) + return { + to: request.txRequest.to as `0x${string}`, + value: BigNumber.from(request.txRequest.value).toBigInt(), + data: request.txRequest.data as `0x${string}`, + } +} - const gasPrice = await parentPublicClient.getGasPrice() - - const nonce = await parentPublicClient.getTransactionCount({ - address: typeof account === 'string' ? account as `0x${string}` : account.address, - blockTag: 'latest' - }) - - const signedTx = await walletClient.signTransaction({ - ...request, - account, - chain: localEthChain, - gas: BigInt('130000'), - maxFeePerGas: gasPrice, - maxPriorityFeePerGas: gasPrice, - nonce - }) +export async function prepareDepositEthToTransaction( + client: Client, + { + amount, + account, + destinationAddress, + parentPublicClient, + }: PrepareDepositEthToParameters +): Promise { + const childProvider = transformPublicClientToProvider(client) + const parentProvider = transformPublicClientToProvider(parentPublicClient) + const ethBridger = await EthBridger.fromProvider(childProvider) - // Parse and serialize with L2 chain ID - const parsedTx = parseTransaction((signedTx as any).raw) - const serializedTx = serializeTransaction({ - ...parsedTx, - }) + const request = await ethBridger.getDepositToRequest({ + amount: BigNumber.from(amount), + destinationAddress, + from: typeof account === 'string' ? account : account.address, + parentProvider, + childProvider, + }) - // Send to L2 - const hash = await parentPublicClient.sendRawTransaction({ - serializedTransaction: serializedTx - }) + return { + to: request.txRequest.to as `0x${string}`, + value: BigNumber.from(request.txRequest.value).toBigInt(), + data: request.txRequest.data as `0x${string}`, + } +} - return hash - } +export function arbitrumDepositActions() { + return function (client: Client) { + return { + prepareDepositEthTransaction: (args: PrepareDepositEthParameters) => + prepareDepositEthTransaction(client, args), + prepareDepositEthToTransaction: (args: PrepareDepositEthToParameters) => + prepareDepositEthToTransaction(client, args), } } -} \ No newline at end of file +} diff --git a/src/experimental/arbitrumDeposit/transformViemToEthers.ts b/src/experimental/arbitrumDeposit/transformViemToEthers.ts new file mode 100644 index 000000000..73d63d875 --- /dev/null +++ b/src/experimental/arbitrumDeposit/transformViemToEthers.ts @@ -0,0 +1,44 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers' +import { Chain, Client, PublicClient, Transport } from 'viem' + +// based on https://wagmi.sh/react/ethers-adapters#reference-implementation +export function publicClientToProvider( + publicClient: PublicClient +) { + const { chain } = publicClient + + if (typeof chain === 'undefined') { + throw new Error(`[publicClientToProvider] "chain" is undefined`) + } + + const network = { + chainId: chain.id, + name: chain.name, + ensAddress: chain.contracts?.ensRegistry?.address, + } + + return new StaticJsonRpcProvider(chain.rpcUrls.default.http[0], network) +} + +function isPublicClient(object: any): object is PublicClient { + return ( + object !== undefined && + object !== null && + typeof object === 'object' && + 'transport' in object && + object.transport !== null && + typeof object.transport === 'object' && + 'url' in object.transport && + typeof object.transport.url === 'string' && + object.type === 'publicClient' + ) +} + +export const transformPublicClientToProvider = ( + provider: PublicClient | Client +): StaticJsonRpcProvider => { + if (isPublicClient(provider)) { + return publicClientToProvider(provider) + } + throw new Error('Invalid provider') +} diff --git a/tests/integration/arbitrumDeposit.test.ts b/tests/integration/arbitrumDeposit.test.ts index 9f6bc822a..f448b6397 100644 --- a/tests/integration/arbitrumDeposit.test.ts +++ b/tests/integration/arbitrumDeposit.test.ts @@ -1,90 +1,157 @@ import { expect } from 'chai' -import { createWalletClient, createPublicClient, http, parseEther } from 'viem' +import { + createWalletClient, + createPublicClient, + http, + parseEther, + type PublicClient, +} from 'viem' import { privateKeyToAccount } from 'viem/accounts' +import { config, testSetup } from '../../scripts/testSetup' import { arbitrumDepositActions } from '../../src/experimental/arbitrumDeposit/actions' -import { testSetup, config } from '../../scripts/testSetup' import { localEthChain, localArbChain } from '../../src/experimental/chains' -describe('arbitrumDepositActions', function() { - this.timeout(60000) - - it('deposits ether', async function() { - const { childChain } = await testSetup() +describe('arbitrumDepositActions', function () { + before(async function () { + await testSetup() + }) + it('deposits ETH from L1 to L2', async function () { const account = privateKeyToAccount(`0x${config.ethKey}` as `0x${string}`) + const depositAmount = parseEther('0.01') - // Create parent clients + // Create L1 clients const parentWalletClient = createWalletClient({ account, chain: localEthChain, - transport: http(config.ethUrl) + transport: http(config.ethUrl), }) const parentPublicClient = createPublicClient({ chain: localEthChain, - transport: http(config.ethUrl) - }).extend(arbitrumDepositActions({ - ethBridge: { - inbox: childChain.ethBridge.inbox as `0x${string}` - } - })) + transport: http(config.ethUrl), + }) - // Create child client for balance checks + // Create L2 client and extend with deposit actions const childPublicClient = createPublicClient({ chain: localArbChain, - transport: http(config.arbUrl) - }) + transport: http(config.arbUrl), + }).extend(arbitrumDepositActions()) + // Get initial L2 balance const initialBalance = await childPublicClient.getBalance({ - address: account.address + address: account.address, }) - console.log('Initial child balance:', initialBalance) + // Prepare and send deposit transaction + const request = await childPublicClient.prepareDepositEthTransaction({ + amount: depositAmount, + account, + }) + + const hash = await parentWalletClient.sendTransaction(request) + + // Wait for L1 transaction + const receipt = await parentPublicClient.waitForTransactionReceipt({ + hash, + }) + + expect(receipt.status).to.equal('success') + + // Wait for L2 balance to increase + let finalBalance = initialBalance + let attempts = 0 + const maxAttempts = 10 + + while (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 3000)) + + const currentBalance = await childPublicClient.getBalance({ + address: account.address, + }) + + if (currentBalance > initialBalance) { + finalBalance = currentBalance + break + } + + attempts++ + } + + const balanceDiff = finalBalance - initialBalance + expect(balanceDiff).to.equal(depositAmount) + }) + + it('deposits ETH from L1 to a different L2 address', async function () { + const account = privateKeyToAccount(`0x${config.ethKey}` as `0x${string}`) + const destinationAddress = + '0x1234567890123456789012345678901234567890' as `0x${string}` const depositAmount = parseEther('0.01') - console.log('Deposit amount:', depositAmount) - const hash = await parentPublicClient.depositEth({ + // Create L1 clients + const parentWalletClient = createWalletClient({ + account, + chain: localEthChain, + transport: http(config.ethUrl), + }) + + const parentPublicClient = createPublicClient({ + chain: localEthChain, + transport: http(config.ethUrl), + }) + + // Create L2 client and extend with deposit actions + const childPublicClient = createPublicClient({ + chain: localArbChain, + transport: http(config.arbUrl), + }).extend(arbitrumDepositActions()) + + // Get initial destination balance + const initialBalance = await childPublicClient.getBalance({ + address: destinationAddress, + }) + + // Prepare and send deposit transaction + const request = await childPublicClient.prepareDepositEthToTransaction({ amount: depositAmount, account: account.address, - walletClient: parentWalletClient + destinationAddress, + parentPublicClient, + }) + + const hash = await parentWalletClient.sendTransaction({ + ...request, + chain: localEthChain, }) - // Wait for parent transaction - const receipt = await parentPublicClient.waitForTransactionReceipt({ + // Wait for L1 transaction + const receipt = await parentPublicClient.waitForTransactionReceipt({ hash, - confirmations: 1 }) expect(receipt.status).to.equal('success') - // Poll for child balance change + // Wait for L2 balance to increase let finalBalance = initialBalance let attempts = 0 const maxAttempts = 10 while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 3000)) - + const currentBalance = await childPublicClient.getBalance({ - address: account.address + address: destinationAddress, }) - console.log(`Attempt ${attempts + 1} - Current balance:`, currentBalance) - if (currentBalance > initialBalance) { finalBalance = currentBalance break } - + attempts++ } - console.log('Final child balance:', finalBalance) - console.log('Balance difference:', finalBalance - initialBalance) - - expect(Number(finalBalance)).to.be.greaterThan( - Number(initialBalance), - 'child balance did not increase after deposit' - ) + const balanceDiff = finalBalance - initialBalance + expect(balanceDiff).to.equal(depositAmount) }) -}) \ No newline at end of file +})