Deposits (viem)
A fast path to deposit ETH / ERC-20 from L1 → ZKsync (L2) using the viem adapter.
Prerequisites
- A funded L1 account (gas + amount).
- RPC URLs:
L1_RPC_URL
,L2_RPC_URL
. - Installed:
@dutterbutter/zksync-sdk
+viem
.
Parameters (quick reference)
Param | Required | Meaning |
---|---|---|
token | Yes | ETH_ADDRESS or ERC-20 address |
amount | Yes | BigInt/wei (e.g. parseEther('0.01') ) |
to | Yes | L2 recipient address |
l2GasLimit | No | L2 execution gas cap |
gasPerPubdata | No | Pubdata price hint |
operatorTip | No | Optional tip to operator |
refundRecipient | No | L2 address to receive fee refunds |
ERC-20 deposits may require an L1
approve()
.quote()
surfaces required steps.
Fast path (one-shot)
// examples/deposit-eth.ts
import { createPublicClient, createWalletClient, http, parseEther, WalletClient } from 'viem';
import type { Account, Chain, Transport } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { createViemSdk, createViemClient } from '@dutterbutter/zksync-sdk/viem';
import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core';
const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX
const L2_RPC = 'http://localhost:3050'; // your L2 RPC
const PRIVATE_KEY = process.env.PRIVATE_KEY || '';
async function main() {
if (!PRIVATE_KEY || PRIVATE_KEY.length !== 66) {
throw new Error('Set your PRIVATE_KEY in the .env file');
}
// --- Viem clients ---
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
const l1 = createPublicClient({ transport: http(L1_RPC) });
const l2 = createPublicClient({ transport: http(L2_RPC) });
const l1Wallet: WalletClient<Transport, Chain, Account> = createWalletClient({
account,
transport: http(L1_RPC),
});
// Check balances
const [balL1, balL2] = await Promise.all([
l1.getBalance({ address: account.address }),
l2.getBalance({ address: account.address }),
]);
console.log('L1 balance:', balL1.toString());
console.log('L2 balance:', balL2.toString());
// client + sdk
const client = createViemClient({ l1, l2, l1Wallet });
const sdk = createViemSdk(client);
const me = account.address;
const params = {
amount: parseEther('0.01'), // 0.01 ETH
to: me,
token: ETH_ADDRESS,
// optional:
// l2GasLimit: 300_000n,
// gasPerPubdata: 800n,
// operatorTip: 0n,
// refundRecipient: me,
} as const;
// Quote
const quote = await sdk.deposits.quote(params);
console.log('QUOTE response:', quote);
// Prepare (route + steps, no sends)
const prepared = await sdk.deposits.prepare(params);
console.log('PREPARE response:', prepared);
// Create (prepare + send)
const created = await sdk.deposits.create(params);
console.log('CREATE response:', created);
// Status (quick check)
const status = await sdk.deposits.status(created);
console.log('STATUS response:', status);
// Wait (L1 inclusion)
const l1Receipt = await sdk.deposits.wait(created, { for: 'l1' });
console.log(
'L1 Included at block:',
l1Receipt?.blockNumber,
'status:',
l1Receipt?.status,
'hash:',
l1Receipt?.transactionHash,
);
// Status again
const status2 = await sdk.deposits.status(created);
console.log('STATUS2 response:', status2);
// Wait for L2 execution
const l2Receipt = await sdk.deposits.wait(created, { for: 'l2' });
console.log(
'L2 Included at block:',
l2Receipt?.blockNumber,
'status:',
l2Receipt?.status,
'hash:',
l2Receipt?.transactionHash,
);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
create()
prepares and sends.wait(..., { for: 'l1' })
⇒ included on L1.wait(..., { for: 'l2' })
⇒ executed on L2 (funds available).
Inspect & customize (quote → prepare → create)
1. Quote (no side-effects)
Preview fees/steps and whether an approve is required.
const quote = await sdk.deposits.quote(params);
2. Prepare (build txs, don’t send)
Get TransactionRequest[]
for signing/UX.
const plan = await sdk.deposits.prepare(params);
3. Create (send) Use defaults, or send your prepared txs if you customized.
const handle = await sdk.deposits.create(params);
Track progress (status vs wait)
Non-blocking snapshot
const s = await sdk.deposits.status(handle /* or l1TxHash */);
// 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED'
Block until checkpoint
const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' });
const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' });
Error handling patterns
Exceptions
try {
const handle = await sdk.deposits.create(params);
} catch (e) {
// normalized error envelope (type, operation, message, context, revert?)
}
No-throw style
Every method has a try*
variant (e.g. tryQuote
, tryPrepare
, tryCreate
).
These never throw—so you don’t need a try/catch
. Instead they return:
{ ok: true, value: ... }
on success{ ok: false, error: ... }
on failure
This is useful for UI flows or services where you want explicit control over errors.
const r = await sdk.deposits.tryCreate(params);
if (!r.ok) {
// handle the error gracefully
console.error('Deposit failed:', r.error);
// maybe show a toast, retry, etc.
} else {
const handle = r.value;
console.log('Deposit sent. L1 tx hash:', handle.l1TxHash);
}
Troubleshooting
- Stuck at L1: check L1 gas and RPC health.
- No L2 execution: verify L2 RPC; re-check
status()
(should move toL2_EXECUTED
). - L2 failed:
status.phase === 'L2_FAILED'
→ inspect revert info via your error envelope/logs.