Skip to content

Commit 15ab5ee

Browse files
feat(zksync): add withdraw action (#3221)
feat(zksync): add `withdraw` action
1 parent e4fd731 commit 15ab5ee

File tree

10 files changed

+969
-0
lines changed

10 files changed

+969
-0
lines changed

.changeset/fuzzy-scissors-carry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"viem": minor
3+
---
4+
5+
Added `withdraw` action in ZKsync extension
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
---
2+
description: Initiates the withdrawal process which withdraws ETH or any ERC20 token from the associated account on L2 network to the target account on L1 network.
3+
---
4+
5+
# withdraw
6+
7+
Initiates the withdrawal process which withdraws ETH or any ERC20 token
8+
from the associated account on L2 network to the target account on L1 network.
9+
10+
## Usage
11+
12+
:::code-group
13+
14+
```ts [example.ts]
15+
import { account, walletClient } from './config'
16+
import { legacyEthAddress } from 'viem/zksync'
17+
18+
const hash = await walletClient.withdraw({
19+
account,
20+
amount: 1_000_000_000_000_000_000n,
21+
token: legacyEthAddress,
22+
})
23+
```
24+
25+
```ts [config.ts]
26+
import { createWalletClient, custom } from 'viem'
27+
import { privateKeyToAccount } from 'viem/accounts'
28+
import { zksync } from 'viem/chains'
29+
import { eip712Actions } from 'viem/zksync'
30+
31+
export const walletClient = createWalletClient({
32+
chain: zksync,
33+
transport: custom(window.ethereum)
34+
}).extend(publicActionsL2())
35+
36+
// JSON-RPC Account
37+
export const [account] = await walletClient.getAddresses()
38+
// Local Account
39+
export const account = privateKeyToAccount(...)
40+
```
41+
42+
:::
43+
44+
### Account Hoisting
45+
46+
If you do not wish to pass an `account` to every `withdraw`, you can also hoist the Account on the Wallet Client (see `config.ts`).
47+
48+
[Learn more](/docs/clients/wallet#account).
49+
50+
:::code-group
51+
52+
```ts [example.ts]
53+
import { walletClient } from './config'
54+
import { legacyEthAddress } from 'viem/zksync'
55+
56+
const hash = await walletClient.withdraw({ // [!code focus:99]
57+
amount: 1_000_000_000_000_000_000n,
58+
token: legacyEthAddress,
59+
})
60+
// '0x...'
61+
```
62+
63+
```ts [config.ts (JSON-RPC Account)]
64+
import { createWalletClient, custom } from 'viem'
65+
import { publicActionsL2 } from 'viem/zksync'
66+
67+
// Retrieve Account from an EIP-712 Provider. // [!code focus]
68+
const [account] = await window.ethereum.request({ // [!code focus]
69+
method: 'eth_requestAccounts' // [!code focus]
70+
}) // [!code focus]
71+
72+
export const walletClient = createWalletClient({
73+
account,
74+
transport: custom(window.ethereum) // [!code focus]
75+
}).extend(publicActionsL2())
76+
```
77+
78+
```ts [config.ts (Local Account)]
79+
import { createWalletClient, custom } from 'viem'
80+
import { privateKeyToAccount } from 'viem/accounts'
81+
import { publicActionsL2 } from 'viem/zksync'
82+
83+
export const walletClient = createWalletClient({
84+
account: privateKeyToAccount('0x...'), // [!code focus]
85+
transport: custom(window.ethereum)
86+
}).extend(publicActionsL2())
87+
```
88+
89+
:::
90+
91+
## Returns
92+
93+
[`Hash`](/docs/glossary/types#hash)
94+
95+
The [Transaction](/docs/glossary/terms#transaction) hash.
96+
97+
## Parameters
98+
99+
### account
100+
101+
- **Type:** `Account | Address`
102+
103+
The Account to send the transaction from.
104+
105+
Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc).
106+
107+
```ts
108+
const hash = await walletClient.withdraw({
109+
account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus]
110+
amount: 1_000_000_000_000_000_000n,
111+
token: legacyEthAddress,
112+
})
113+
```
114+
115+
### amount
116+
117+
- **Type:** `bigint`
118+
119+
The amount of the token to withdraw.
120+
121+
```ts
122+
const hash = await walletClient.withdraw({
123+
account,
124+
amount: 1_000_000_000_000_000_000n, // [!code focus]
125+
token: legacyEthAddress,
126+
})
127+
```
128+
129+
### token
130+
131+
- **Type:** `Address`
132+
133+
The address of the token on L2.
134+
135+
```ts
136+
const hash = await walletClient.withdraw({
137+
account,
138+
amount: 1_000_000_000_000_000_000n,
139+
token: legacyEthAddress, // [!code focus]
140+
})
141+
```
142+
143+
### bridgeAddress (optional)
144+
145+
- **Type:** `Address`
146+
147+
The address of the bridge contract to be used. By default, uses shared bridge.
148+
149+
```ts
150+
const address = await walletClient.withdraw({
151+
account,
152+
amount: 1_000_000_000_000_000_000n,
153+
token: legacyEthAddress,
154+
bridgeAddress: '0xf8c919286126ccf2e8abc362a15158a461429c82' // [!code focus]
155+
})
156+
```
157+
158+
### chain (optional)
159+
160+
- **Type:** [`Chain`](/docs/glossary/types#chain)
161+
- **Default:** `walletClient.chain`
162+
163+
The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown.
164+
165+
The chain is also used to infer its request type.
166+
167+
```ts
168+
import { zksync } from 'viem/chains' // [!code focus]
169+
170+
const hash = await walletClient.withdraw({
171+
chain: zksync, // [!code focus]
172+
account,
173+
amount: 1_000_000_000_000_000_000n,
174+
token: legacyEthAddress,
175+
})
176+
```
177+
178+
### gasPerPubdata (optional)
179+
180+
- **Type:** `bigint`
181+
182+
The amount of gas for publishing one byte of data on Ethereum.
183+
184+
```ts
185+
const hash = await walletClient.withdraw({
186+
account,
187+
amount: 1_000_000_000_000_000_000n,
188+
token: legacyEthAddress,
189+
gasPerPubdata: 50000, // [!code focus]
190+
})
191+
```
192+
193+
### gasPrice (optional)
194+
195+
- **Type:** `bigint`
196+
197+
The price (in wei) to pay per gas. Only applies to [Legacy Transactions](/docs/glossary/terms#legacy-transaction).
198+
199+
```ts
200+
const hash = await walletClient.withdraw({
201+
account,
202+
amount: 1_000_000_000_000_000_000n,
203+
token: legacyEthAddress,
204+
gasPrice: parseGwei('20'), // [!code focus]
205+
})
206+
```
207+
208+
### maxFeePerGas (optional)
209+
210+
- **Type:** `bigint`
211+
212+
Total fee per gas (in wei), inclusive of `maxPriorityFeePerGas`. Only applies to [EIP-1559 Transactions](/docs/glossary/terms#eip-1559-transaction)
213+
214+
```ts
215+
const hash = await walletClient.withdraw({
216+
account,
217+
amount: 1_000_000_000_000_000_000n,
218+
token: legacyEthAddress,
219+
maxFeePerGas: parseGwei('20'), // [!code focus]
220+
})
221+
```
222+
223+
### maxPriorityFeePerGas (optional)
224+
225+
- **Type:** `bigint`
226+
227+
Max priority fee per gas (in wei). Only applies to [EIP-1559 Transactions](/docs/glossary/terms#eip-1559-transaction)
228+
229+
```ts
230+
const hash = await walletClient.withdraw({
231+
account,
232+
amount: 1_000_000_000_000_000_000n,
233+
token: legacyEthAddress,
234+
maxFeePerGas: parseGwei('20'),
235+
maxPriorityFeePerGas: parseGwei('2'), // [!code focus]
236+
})
237+
```
238+
239+
### nonce (optional)
240+
241+
- **Type:** `number`
242+
243+
Unique number identifying this transaction.
244+
245+
```ts
246+
const hash = await walletClient.withdraw({
247+
account,
248+
amount: 1_000_000_000_000_000_000n,
249+
token: legacyEthAddress,
250+
nonce: 69 // [!code focus]
251+
})
252+
```
253+
254+
255+
### paymaster (optional)
256+
257+
- **Type:** `Account | Address`
258+
259+
Address of the paymaster account that will pay the fees. The `paymasterInput` field is required with this one.
260+
261+
```ts
262+
const hash = await walletClient.withdraw({
263+
account,
264+
amount: 1_000_000_000_000_000_000n,
265+
token: legacyEthAddress,
266+
paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus]
267+
paymasterInput: '0x8c5a...' // [!code focus]
268+
})
269+
```
270+
271+
### paymasterInput (optional)
272+
273+
- **Type:** `0x${string}`
274+
275+
Input data to the paymaster. The `paymaster` field is required with this one.
276+
277+
```ts
278+
const hash = await walletClient.withdraw({
279+
account,
280+
amount: 1_000_000_000_000_000_000n,
281+
token: legacyEthAddress,
282+
paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus]
283+
paymasterInput: '0x8c5a...' // [!code focus]
284+
})
285+
```
286+
287+
### to (optional)
288+
289+
- **Type:** `Address`
290+
291+
The address of the recipient on L1. Defaults to the sender address.
292+
293+
```ts
294+
const hash = await walletClient.withdraw({
295+
account,
296+
amount: 1_000_000_000_000_000_000n,
297+
token: legacyEthAddress,
298+
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus]
299+
})
300+
```

site/sidebar.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,15 @@ export const sidebar = {
17881788
},
17891789
],
17901790
},
1791+
{
1792+
text: 'L2 Wallet Actions',
1793+
items: [
1794+
{
1795+
text: 'withdraw',
1796+
link: '/zksync/actions/withdraw',
1797+
},
1798+
],
1799+
},
17911800
{
17921801
text: 'L1 Wallet Actions',
17931802
items: [
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect, test } from 'vitest'
2+
3+
import { anvilZksync } from '~test/src/anvil.js'
4+
import { accounts } from '~test/src/constants.js'
5+
import { mockRequestReturnData } from '~test/src/zksync.js'
6+
import { privateKeyToAccount } from '~viem/accounts/privateKeyToAccount.js'
7+
import type { EIP1193RequestFn } from '~viem/index.js'
8+
import { legacyEthAddress, publicActionsL2 } from '~viem/zksync/index.js'
9+
import { withdraw } from './withdraw.js'
10+
11+
const request = (async ({ method, params }) => {
12+
if (method === 'eth_sendRawTransaction')
13+
return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87'
14+
if (method === 'eth_sendTransaction')
15+
return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87'
16+
if (method === 'eth_call')
17+
return '0x00000000000000000000000070a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55'
18+
if (method === 'eth_estimateGas') return 158774n
19+
if (method === 'eth_getTransactionCount') return 1n
20+
if (method === 'eth_getBlockByNumber') return anvilZksync.forkBlockNumber
21+
if (method === 'eth_gasPrice') return 200_000_000_000n
22+
if (method === 'eth_chainId') return anvilZksync.chain.id
23+
return (
24+
(await mockRequestReturnData(method)) ??
25+
(await anvilZksync.getClient().request({ method, params } as any))
26+
)
27+
}) as EIP1193RequestFn
28+
29+
const baseClient = anvilZksync.getClient({ batch: { multicall: false } })
30+
baseClient.request = request
31+
const client = baseClient.extend(publicActionsL2())
32+
33+
const baseClientWithAccount = anvilZksync.getClient({
34+
account: true,
35+
batch: { multicall: false },
36+
})
37+
baseClientWithAccount.request = request
38+
const clientWithAccount = baseClientWithAccount.extend(publicActionsL2())
39+
40+
test.skip('default', async () => {
41+
expect(
42+
await withdraw(client, {
43+
account: privateKeyToAccount(accounts[0].privateKey),
44+
amount: 7_000_000_000n,
45+
token: legacyEthAddress,
46+
}),
47+
).toBeDefined()
48+
})
49+
50+
test('default: account hoisting', async () => {
51+
expect(
52+
await withdraw(clientWithAccount, {
53+
amount: 7_000_000_000n,
54+
token: legacyEthAddress,
55+
}),
56+
).toBeDefined()
57+
})
58+
59+
test('errors: no account provided', async () => {
60+
await expect(() =>
61+
withdraw(client, {
62+
amount: 7_000_000_000n,
63+
token: legacyEthAddress,
64+
}),
65+
).rejects.toThrowErrorMatchingInlineSnapshot(`
66+
[AccountNotFoundError: Could not find an Account to execute with this Action.
67+
Please provide an Account with the \`account\` argument on the Action, or by supplying an \`account\` to the Client.
68+
69+
Docs: https://viem.sh/docs/actions/wallet/sendTransaction
70+
71+
`)
72+
})

0 commit comments

Comments
 (0)