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
l1TxOverridesNoL1 tx overrides (e.g. gasLimit, maxFeePerGas, maxPriorityFeePerGas)

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