-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
215 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <TClient extends PublicClient>(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<TransactionRequest> { | ||
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<TransactionRequest> { | ||
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), | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TChain extends Chain | undefined>( | ||
publicClient: PublicClient<Transport, TChain> | ||
) { | ||
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') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
}) | ||
}) |