diff --git a/src/experimental/actions.ts b/src/experimental/actions.ts index 24687d8c0..6a2a74e89 100644 --- a/src/experimental/actions.ts +++ b/src/experimental/actions.ts @@ -1,27 +1,48 @@ import { BigNumber } from 'ethers' -import { Account, Address, PublicClient, TransactionRequest } from 'viem' +import { + Account, + Address, + Hash, + PublicClient, + TransactionRequest, + WalletClient, +} from 'viem' import { EthBridger } from '../lib/assetBridger/ethBridger' -import { transformPublicClientToProvider } from './transformViemToEthers' +import { + transformPublicClientToProvider, + viemTransactionReceiptToEthersTransactionReceipt, +} from './transformViemToEthers' +import { ParentTransactionReceipt } from '../lib/message/ParentTransaction' +import { ParentToChildMessageStatus } from '../lib/message/ParentToChildMessage' export type PrepareDepositEthParameters = { amount: bigint account: Account | Address } -export type PrepareDepositEthToParameters = { - amount: bigint - account: Address - destinationAddress: Address - parentPublicClient: PublicClient +export type WaitForCrossChainTxParameters = { + hash: Hash + timeout?: number + confirmations?: number +} + +export type CrossChainTransactionStatus = { + status: 'success' | 'failed' + complete: boolean + message?: unknown + childTxReceipt?: unknown } export type ArbitrumDepositActions = { prepareDepositEthTransaction: ( params: PrepareDepositEthParameters ) => Promise - prepareDepositEthToTransaction: ( - params: PrepareDepositEthToParameters - ) => Promise +} + +export type ArbitrumParentWalletActions = { + waitForCrossChainTransaction: ( + params: WaitForCrossChainTxParameters + ) => Promise } async function prepareDepositEthTransaction( @@ -42,39 +63,74 @@ async function prepareDepositEthTransaction( } } -async function prepareDepositEthToTransaction( - client: PublicClient, - { - amount, - account, - destinationAddress, - parentPublicClient, - }: PrepareDepositEthToParameters -): Promise { - const childProvider = transformPublicClientToProvider(client) - const parentProvider = transformPublicClientToProvider(parentPublicClient) - const ethBridger = await EthBridger.fromProvider(childProvider) +async function waitForCrossChainTransaction( + parentClient: PublicClient, + childClient: PublicClient, + { hash, timeout, confirmations }: WaitForCrossChainTxParameters +): Promise { + const childProvider = transformPublicClientToProvider(childClient) - const request = await ethBridger.getDepositToRequest({ - amount: BigNumber.from(amount), - destinationAddress, - from: account, - parentProvider, - childProvider, + // Wait for the transaction to be mined and get the receipt + const viemReceipt = await parentClient.waitForTransactionReceipt({ + hash, + confirmations, }) - return { - to: request.txRequest.to as `0x${string}`, - value: BigNumber.from(request.txRequest.value).toBigInt(), - data: request.txRequest.data as `0x${string}`, + const ethersReceipt = + viemTransactionReceiptToEthersTransactionReceipt(viemReceipt) + const parentReceipt = new ParentTransactionReceipt(ethersReceipt) + + // Try to get eth deposits first + try { + const ethDeposits = await parentReceipt.getEthDeposits(childProvider) + if (ethDeposits.length > 0) { + const result = await ethDeposits[0].wait(confirmations, timeout) + return { + status: result ? 'success' : 'failed', + complete: Boolean(result), + message: ethDeposits[0], + childTxReceipt: result, + } + } + } catch (e) { + // Not an eth deposit, continue to check for other message types + } + + // Check for other cross chain messages + try { + const messages = await parentReceipt.getParentToChildMessages(childProvider) + if (messages.length > 0) { + const result = await messages[0].waitForStatus(confirmations, timeout) + return { + status: + result.status === ParentToChildMessageStatus.REDEEMED + ? 'success' + : 'failed', + complete: result.status === ParentToChildMessageStatus.REDEEMED, + message: messages[0], + childTxReceipt: result, + } + } + } catch (e) { + // Not a cross chain message } + + throw new Error('No cross chain message found in transaction') } export function arbitrumDepositActions() { return (client: PublicClient): ArbitrumDepositActions => ({ prepareDepositEthTransaction: params => prepareDepositEthTransaction(client, params), - prepareDepositEthToTransaction: params => - prepareDepositEthToTransaction(client, params), + }) +} + +export function arbitrumParentWalletActions( + parentClient: PublicClient, + childClient: PublicClient +) { + return (walletClient: WalletClient): ArbitrumParentWalletActions => ({ + waitForCrossChainTransaction: (params: WaitForCrossChainTxParameters) => + waitForCrossChainTransaction(parentClient, childClient, params), }) } diff --git a/src/experimental/createArbitrumClient.ts b/src/experimental/createArbitrumClient.ts index 98e972d54..3b4101d97 100644 --- a/src/experimental/createArbitrumClient.ts +++ b/src/experimental/createArbitrumClient.ts @@ -1,9 +1,22 @@ -import { Chain, PublicClient, createPublicClient, http } from 'viem' -import { arbitrumDepositActions, ArbitrumDepositActions } from './actions' +import { + Chain, + PublicClient, + WalletClient, + createPublicClient, + http, +} from 'viem' +import { + ArbitrumDepositActions, + ArbitrumParentWalletActions, + arbitrumDepositActions, + arbitrumParentWalletActions, +} from './actions' export type ArbitrumClients = { parentPublicClient: PublicClient childPublicClient: PublicClient & ArbitrumDepositActions + parentWalletClient: WalletClient & ArbitrumParentWalletActions + childWalletClient: WalletClient } export type CreateArbitrumClientParams = { @@ -11,6 +24,8 @@ export type CreateArbitrumClientParams = { childChain: Chain parentRpcUrl?: string childRpcUrl?: string + parentWalletClient: WalletClient + childWalletClient: WalletClient } export function createArbitrumClient({ @@ -18,6 +33,8 @@ export function createArbitrumClient({ childChain, parentRpcUrl, childRpcUrl, + parentWalletClient, + childWalletClient, }: CreateArbitrumClientParams): ArbitrumClients { const parentPublicClient = createPublicClient({ chain: parentChain, @@ -29,8 +46,14 @@ export function createArbitrumClient({ transport: http(childRpcUrl || childChain.rpcUrls.default.http[0]), }).extend(arbitrumDepositActions()) + const extendedParentWalletClient = parentWalletClient.extend( + arbitrumParentWalletActions(parentPublicClient, childPublicClient) + ) + return { parentPublicClient, childPublicClient, + parentWalletClient: extendedParentWalletClient, + childWalletClient, } } diff --git a/src/experimental/transformViemToEthers.ts b/src/experimental/transformViemToEthers.ts index 73d63d875..9acee96d4 100644 --- a/src/experimental/transformViemToEthers.ts +++ b/src/experimental/transformViemToEthers.ts @@ -1,5 +1,17 @@ +import { + Log as EthersLog, + TransactionReceipt as EthersTransactionReceipt, +} from '@ethersproject/abstract-provider' import { StaticJsonRpcProvider } from '@ethersproject/providers' -import { Chain, Client, PublicClient, Transport } from 'viem' +import { BigNumber } from 'ethers' +import { + Chain, + Client, + PublicClient, + Transport, + Log as ViemLog, + TransactionReceipt as ViemTransactionReceipt, +} from 'viem' // based on https://wagmi.sh/react/ethers-adapters#reference-implementation export function publicClientToProvider( @@ -42,3 +54,42 @@ export const transformPublicClientToProvider = ( } throw new Error('Invalid provider') } + +function viemLogToEthersLog(log: ViemLog): EthersLog { + return { + blockNumber: Number(log.blockNumber), + blockHash: log.blockHash!, + transactionIndex: log.transactionIndex!, + removed: log.removed, + address: log.address, + data: log.data, + topics: log.topics, + transactionHash: log.transactionHash!, + logIndex: log.logIndex!, + } +} + +export function viemTransactionReceiptToEthersTransactionReceipt( + receipt: ViemTransactionReceipt +): EthersTransactionReceipt { + return { + to: receipt.to!, + from: receipt.from!, + contractAddress: receipt.contractAddress!, + transactionIndex: receipt.transactionIndex, + gasUsed: BigNumber.from(receipt.gasUsed), + logsBloom: receipt.logsBloom, + blockHash: receipt.blockHash, + transactionHash: receipt.transactionHash, + logs: receipt.logs.map(log => viemLogToEthersLog(log)), + blockNumber: Number(receipt.blockNumber), + // todo: if we need this we can add it later + confirmations: -1, + cumulativeGasUsed: BigNumber.from(receipt.cumulativeGasUsed), + effectiveGasPrice: BigNumber.from(receipt.effectiveGasPrice), + // all transactions that we care about are well past byzantium + byzantium: true, + type: Number(receipt.type), + status: receipt.status === 'success' ? 1 : 0, + } +} diff --git a/tests/integration/arbitrumDepositActions.test.ts b/tests/integration/arbitrumDepositActions.test.ts index e10983dc7..e21412fa2 100644 --- a/tests/integration/arbitrumDepositActions.test.ts +++ b/tests/integration/arbitrumDepositActions.test.ts @@ -18,6 +18,7 @@ describe('arbitrumDepositActions', function () { const setup = await testSetup() localEthChain = setup.localEthChain localArbChain = setup.localArbChain + await fundParentSigner(setup.parentSigner) if (isArbitrumNetworkWithCustomFeeToken()) { await fundParentCustomFeeToken(setup.parentSigner) @@ -25,129 +26,56 @@ describe('arbitrumDepositActions', function () { } }) - it('deposits ETH from parent to child', async function () { - const account = privateKeyToAccount(`0x${config.ethKey}` as `0x${string}`) + it('deposits ETH from parent to child and waits for completion', async function () { + const parentAccount = privateKeyToAccount( + `0x${config.ethKey}` as `0x${string}` + ) const depositAmount = parseEther('0.01') - const parentWalletClient = createWalletClient({ - account, + const baseParentWalletClient = createWalletClient({ + account: parentAccount, chain: localEthChain, transport: http(config.ethUrl), }) - const { parentPublicClient, childPublicClient } = createArbitrumClient({ - parentChain: localEthChain, - childChain: localArbChain, - parentRpcUrl: config.ethUrl, - childRpcUrl: config.arbUrl, - }) - - const initialBalance = await childPublicClient.getBalance({ - address: account.address, - }) - - const request = await childPublicClient.prepareDepositEthTransaction({ - amount: depositAmount, - account, - }) - - const hash = await parentWalletClient.sendTransaction({ - ...request, - chain: localEthChain, - account, - kzg: undefined, - } as const) - - const receipt = await parentPublicClient.waitForTransactionReceipt({ - hash, - }) - - expect(receipt.status).to.equal('success') - - let finalBalance = initialBalance - let attempts = 0 - const maxAttempts = 12 - - while (attempts < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, 1500)) - - 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 parent to a different child address', async function () { - const account = privateKeyToAccount(`0x${config.ethKey}` as `0x${string}`) - const destinationAddress = - '0x1234567890123456789012345678901234567890' as `0x${string}` - const depositAmount = parseEther('0.01') - - const parentWalletClient = createWalletClient({ - account, - chain: localEthChain, - transport: http(config.ethUrl), + const baseChildWalletClient = createWalletClient({ + account: parentAccount, + chain: localArbChain, + transport: http(config.arbUrl), }) - const { parentPublicClient, childPublicClient } = createArbitrumClient({ + const { childPublicClient, parentWalletClient } = createArbitrumClient({ parentChain: localEthChain, childChain: localArbChain, - parentRpcUrl: config.ethUrl, - childRpcUrl: config.arbUrl, + parentWalletClient: baseParentWalletClient, + childWalletClient: baseChildWalletClient, }) const initialBalance = await childPublicClient.getBalance({ - address: destinationAddress, + address: parentAccount.address, }) - const request = await childPublicClient.prepareDepositEthToTransaction({ + const request = await childPublicClient.prepareDepositEthTransaction({ amount: depositAmount, - account: account.address, - destinationAddress, - parentPublicClient, + account: parentAccount, }) const hash = await parentWalletClient.sendTransaction({ ...request, chain: localEthChain, - account, - kzg: undefined, - } as const) + account: parentAccount, + }) - const receipt = await parentPublicClient.waitForTransactionReceipt({ + const result = await parentWalletClient.waitForCrossChainTransaction({ hash, }) - expect(receipt.status).to.equal('success') - - let finalBalance = initialBalance - let attempts = 0 - const maxAttempts = 12 + expect(result.status).to.equal('success') + expect(result.complete).to.be.true - while (attempts < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, 1500)) - - const currentBalance = await childPublicClient.getBalance({ - address: destinationAddress, - }) - - if (currentBalance > initialBalance) { - finalBalance = currentBalance - break - } - - attempts++ - } + const finalBalance = await childPublicClient.getBalance({ + address: parentAccount.address, + }) const balanceDiff = finalBalance - initialBalance expect(balanceDiff).to.equal(depositAmount)