Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds sdk-viem withdraws #566

Draft
wants to merge 9 commits into
base: dl/viem-package-init
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 159 additions & 26 deletions packages/sdk-viem/src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
ChildToParentMessageStatus,
ChildTransactionReceipt,
EthBridger,
ParentToChildMessageStatus,
ParentTransactionReceipt,
Expand All @@ -17,14 +19,10 @@ import {
WalletClient,
} from 'viem'

export type PrepareDepositEthParameters = {
amount: bigint
account: Account | Address
}

const DEFAULT_CONFIRMATIONS = 1
const DEFAULT_TIMEOUT = 1000 * 60 * 5 // 5 minutes
export const DEFAULT_CONFIRMATIONS = 1
export const DEFAULT_TIMEOUT = 1000 * 60 * 5 // 5 minutes

// Cross-chain transaction types
export type WaitForCrossChainTxParameters = {
hash: Hash
timeout?: number
Expand All @@ -45,9 +43,13 @@ export type CrossChainTransactionStatus = {
hash: Hash
}

export type DepositEthParameters = {
// Deposit types
export type PrepareDepositEthParameters = {
amount: bigint
account: Account | Address
}

export type DepositEthParameters = PrepareDepositEthParameters & {
confirmations?: number
timeout?: number
}
Expand All @@ -58,6 +60,32 @@ export type ArbitrumDepositActions = {
) => Promise<TransactionRequest>
}

// Withdraw types
export type PrepareWithdrawEthParameters = {
amount: bigint
destinationAddress: Address
account: Account | Address
}

export type WithdrawEthParameters = PrepareWithdrawEthParameters & {
confirmations?: number
timeout?: number
}

export type ArbitrumChildWalletActions = {
prepareWithdrawEthTransaction: (
params: PrepareWithdrawEthParameters
) => Promise<{
request: TransactionRequest
l1GasEstimate: bigint
}>

withdrawEth: (
params: WithdrawEthParameters
) => Promise<CrossChainTransactionStatus>
}

// Parent wallet types
export type ArbitrumParentWalletActions = {
waitForCrossChainTransaction: (
params: WaitForCrossChainTxParameters
Expand All @@ -72,24 +100,6 @@ export type ArbitrumParentWalletActions = {
) => Promise<CrossChainTransactionStatus>
}

export async function prepareDepositEthTransaction(
client: PublicClient,
{ amount, account }: PrepareDepositEthParameters
): Promise<TransactionRequest> {
const provider = publicClientToProvider(client)
const ethBridger = await EthBridger.fromProvider(provider)
const request = await ethBridger.getDepositRequest({
amount: BigNumber.from(amount),
from: typeof account === 'string' ? account : account.address,
})

return {
to: request.txRequest.to as Address,
value: BigNumber.from(request.txRequest.value).toBigInt(),
data: request.txRequest.data as Address,
}
}

export async function waitForCrossChainTransaction(
parentClient: PublicClient,
childClient: PublicClient,
Expand Down Expand Up @@ -164,6 +174,7 @@ export async function sendCrossChainTransaction(
...request,
chain: walletClient.chain,
account: walletClient.account as Account,
kzg: undefined,
})

return waitForCrossChainTransaction(parentClient, childClient, {
Expand All @@ -173,6 +184,25 @@ export async function sendCrossChainTransaction(
})
}

// Deposit functions
export async function prepareDepositEthTransaction(
client: PublicClient,
{ amount, account }: PrepareDepositEthParameters
): Promise<TransactionRequest> {
const provider = publicClientToProvider(client)
const ethBridger = await EthBridger.fromProvider(provider)
const request = await ethBridger.getDepositRequest({
amount: BigNumber.from(amount),
from: typeof account === 'string' ? account : account.address,
})

return {
to: request.txRequest.to as Address,
value: BigNumber.from(request.txRequest.value).toBigInt(),
data: request.txRequest.data as Address,
}
}

export async function depositEth(
parentClient: PublicClient,
childClient: PublicClient,
Expand All @@ -196,6 +226,97 @@ export async function depositEth(
})
}

// Withdraw functions
export async function prepareWithdrawEthTransaction(
client: PublicClient,
{ amount, destinationAddress, account }: PrepareWithdrawEthParameters
): Promise<{
request: TransactionRequest
l1GasEstimate: bigint
}> {
const provider = publicClientToProvider(client)
const ethBridger = await EthBridger.fromProvider(provider)
const request = await ethBridger.getWithdrawalRequest({
amount: BigNumber.from(amount),
destinationAddress,
from: typeof account === 'string' ? account : account.address,
})

const l1GasEstimate = await request.estimateParentGasLimit(provider)

return {
request: {
to: request.txRequest.to as `0x${string}`,
value: BigNumber.from(request.txRequest.value).toBigInt(),
data: request.txRequest.data as `0x${string}`,
},
l1GasEstimate: l1GasEstimate.toBigInt(),
}
}

export async function withdrawEth(
parentClient: PublicClient,
childClient: PublicClient,
walletClient: WalletClient,
{
amount,
destinationAddress,
account,
confirmations = DEFAULT_CONFIRMATIONS,
}: WithdrawEthParameters
): Promise<CrossChainTransactionStatus> {
const { request } = await prepareWithdrawEthTransaction(childClient, {
amount,
destinationAddress,
account,
})

const hash = await walletClient.sendTransaction({
...request,
chain: walletClient.chain,
account: walletClient.account as Account,
kzg: undefined,
})

const childProvider = publicClientToProvider(childClient)
const parentProvider = publicClientToProvider(parentClient)

const viemReceipt = await childClient.waitForTransactionReceipt({
hash,
confirmations,
})

const ethersReceipt =
viemTransactionReceiptToEthersTransactionReceipt(viemReceipt)

const childReceipt = new ChildTransactionReceipt(ethersReceipt)

const messages = await childReceipt.getChildToParentMessages(parentProvider)
if (messages.length === 0) {
return {
status: 'failed',
complete: false,
hash,
message: undefined,
childTxReceipt: undefined,
}
}

const message = messages[0]
const messageStatus = await message.status(childProvider)

// For withdrawals, return early since it needs to wait for challenge period
const isUnconfirmed = messageStatus === ChildToParentMessageStatus.UNCONFIRMED
return {
status: isUnconfirmed ? 'success' : 'failed',
complete: false, // Not complete until executed after challenge period
message,
childTxReceipt: ethersReceipt,
hash,
}
}

// Client action creators
export function arbitrumParentClientActions() {
return (client: PublicClient): ArbitrumDepositActions => ({
prepareDepositEthTransaction: params =>
Expand All @@ -221,3 +342,15 @@ export function arbitrumParentWalletActions(
depositEth(parentClient, childClient, walletClient, params),
})
}

export function arbitrumChildWalletActions(
parentClient: PublicClient,
childClient: PublicClient
) {
return (walletClient: WalletClient): ArbitrumChildWalletActions => ({
prepareWithdrawEthTransaction: (params: PrepareWithdrawEthParameters) =>
prepareWithdrawEthTransaction(childClient, params),
withdrawEth: (params: WithdrawEthParameters) =>
withdrawEth(parentClient, childClient, walletClient, params),
})
}
10 changes: 8 additions & 2 deletions packages/sdk-viem/src/createArbitrumClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
http,
} from 'viem'
import {
ArbitrumChildWalletActions,
arbitrumChildWalletActions,
ArbitrumParentWalletActions,
arbitrumParentWalletActions,
} from './actions'
Expand All @@ -14,7 +16,7 @@ export type ArbitrumClients = {
parentPublicClient: PublicClient
childPublicClient: PublicClient
parentWalletClient: WalletClient & ArbitrumParentWalletActions
childWalletClient?: WalletClient
childWalletClient?: WalletClient & ArbitrumChildWalletActions
}

export type CreateArbitrumClientParams = {
Expand Down Expand Up @@ -48,10 +50,14 @@ export function createArbitrumClient({
arbitrumParentWalletActions(parentPublicClient, childPublicClient)
)

const extendedChildWalletClient = childWalletClient?.extend(
arbitrumChildWalletActions(parentPublicClient, childPublicClient)
)

return {
parentPublicClient,
childPublicClient,
parentWalletClient: extendedParentWalletClient,
childWalletClient,
childWalletClient: extendedChildWalletClient,
}
}
65 changes: 65 additions & 0 deletions packages/sdk-viem/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
ChildToParentMessage,
ChildToParentMessageStatus,
ChildTransactionReceipt,
} from '@arbitrum/sdk'
import { config } from '@arbitrum/sdk/tests/testSetup'
import {
publicClientToProvider,
viemTransactionReceiptToEthersTransactionReceipt,
} from '@offchainlabs/ethers-viem-compat'
import { Wallet } from 'ethers'
import { Hash, PublicClient, TransactionReceipt } from 'viem'

/**
* Test utility to execute a withdrawal after it's been confirmed.
*/
export async function executeConfirmedWithdrawal(
viemReceipt: TransactionReceipt,
childClient: PublicClient,
parentClient: PublicClient,
confirmations = 1
): Promise<{ status: boolean; hash: Hash }> {
const childProvider = publicClientToProvider(childClient)
const parentProvider = publicClientToProvider(parentClient)

const ethersReceipt =
viemTransactionReceiptToEthersTransactionReceipt(viemReceipt)

const childReceipt = new ChildTransactionReceipt(ethersReceipt)

const messages = await childReceipt.getChildToParentMessages(parentProvider)
if (messages.length === 0) {
throw new Error('No messages found in receipt')
}

const message = messages[0]

// Wait for message to be ready to execute
await message.waitUntilReadyToExecute(childProvider)

// Check if message has been confirmed
const status = await message.status(childProvider)
if (status !== ChildToParentMessageStatus.CONFIRMED) {
throw new Error('Message not confirmed after waiting')
}

// Create a writer to execute the message
const parentSigner = new Wallet(`0x${config.ethKey}`, parentProvider)
const events = childReceipt.getChildToParentEvents()
const messageWriter = ChildToParentMessage.fromEvent(parentSigner, events[0])

// Execute the message
const execTx = await messageWriter.execute(childProvider)
const execHash = execTx.hash

const execReceipt = await parentClient.waitForTransactionReceipt({
hash: execHash as `0x${string}`,
confirmations,
})

return {
status: Boolean(execReceipt.status),
hash: execHash as Hash,
}
}
15 changes: 12 additions & 3 deletions packages/sdk-viem/tests/testSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ export type ViemTestSetup = {
localEthChain: Chain
localArbChain: Chain
parentAccount: ReturnType<typeof privateKeyToAccount>
childPublicClient: ArbitrumClients['childPublicClient']
parentPublicClient: ArbitrumClients['parentPublicClient']
parentWalletClient: ArbitrumClients['parentWalletClient']
childPublicClient: ArbitrumClients['childPublicClient']
childWalletClient: ArbitrumClients['childWalletClient']
childChain: Awaited<ReturnType<typeof sdkTestSetup>>['childChain']
parentSigner: Awaited<ReturnType<typeof sdkTestSetup>>['parentSigner']
}
} & Awaited<ReturnType<typeof sdkTestSetup>>

function generateViemChain(
networkData: {
Expand Down Expand Up @@ -76,7 +78,12 @@ export async function testSetup(): Promise<ViemTestSetup> {
transport: http(config.arbUrl),
})

const { childPublicClient, parentWalletClient } = createArbitrumClient({
const {
childPublicClient,
childWalletClient,
parentWalletClient,
parentPublicClient,
} = createArbitrumClient({
parentChain: localEthChain,
childChain: localArbChain,
parentWalletClient: baseParentWalletClient,
Expand All @@ -89,7 +96,9 @@ export async function testSetup(): Promise<ViemTestSetup> {
localArbChain,
parentAccount,
childPublicClient,
childWalletClient,
parentWalletClient,
parentPublicClient,
}
}

Expand Down
Loading
Loading