Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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)

ParamRequiredMeaning
tokenYesETH_ADDRESS or ERC-20 address
amountYesBigInt/wei (e.g. parseEther('0.01'))
toYesL2 recipient address
l2GasLimitNoL2 execution gas cap
gasPerPubdataNoPubdata price hint
operatorTipNoOptional tip to operator
refundRecipientNoL2 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 to L2_EXECUTED).
  • L2 failed: status.phase === 'L2_FAILED' → inspect revert info via your error envelope/logs.

See also