Deposits

L1 → L2 deposits for ETH and ERC-20 tokens with quote, prepare, create, status, and wait helpers using the Viem adapter.


At a Glance

  • Resource: sdk.deposits
  • Common flow: quote → create → wait({ for: 'l2' })
  • Auto-routing: ETH vs ERC-20 and base-token vs non-base handled automatically
  • Error style: Throwing methods (quote, prepare, create, wait) + safe variants (tryQuote, tryPrepare, tryCreate, tryWait)
  • Token mapping: Use sdk.tokens for L1⇄L2 token lookups and assetIds if you need token metadata ahead of time.

Import

import { createViemClient, createViemSdk } from '@matterlabs/zksync-js/viem';
import { privateKeyToAccount } from 'viem/accounts';
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const l1 = createPublicClient({ transport: http(process.env.L1_RPC!) });
const l2 = createPublicClient({ transport: http(process.env.L2_RPC!) });
const l1Wallet = createWalletClient({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) });
const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) });

const client = createViemClient({ l1, l2, l1Wallet, l2Wallet });
const sdk = createViemSdk(client);
// sdk.deposits → DepositsResource

Quick Start

Deposit 0.1 ETH from L1 → L2 and wait for L2 execution:

const depositHandle = await sdk.deposits.create({
  token: ETH_ADDRESS,
  amount: parseEther('0.1'),
  to: account.address,
});

const l2TxReceipt = await sdk.deposits.wait(depositHandle, { for: 'l2' }); // null only if no L1 hash

[!TIP] For UX that never throws, use the try* variants and branch on ok.


Route Selection (Automatic)

RouteMeaning
eth-baseETH when L2 base token is ETH
eth-nonbaseETH when L2 base token ≠ ETH
erc20-baseERC-20 that is the L2 base token
erc20-nonbaseERC-20 that is not the L2 base token

You do not pass a route; it’s derived automatically from chain metadata + token.

Method Reference

quote(p: DepositParams) → Promise<DepositQuote>

Estimate the deposit operation (route, approvals, gas hints). Does not send transactions.

Route-specific fee modeling details are documented in Gas & Fees.

Parameters

NameTypeRequiredDescription
tokenAddress✅L1 token address. Use 0x…00 for ETH.
amountbigint✅Amount in wei to deposit.
toAddress❌L2 recipient address. Defaults to the signer’s address if omitted.
refundRecipientAddress❌Optional address on L1 to receive refunds for unspent gas.
l2GasLimitbigint❌Optional manual L2 gas limit override.
gasPerPubdatabigint❌Optional custom gas-per-pubdata value.
operatorTipbigint❌Optional operator tip (in wei) for priority execution.
l1TxOverridesEip1559GasOverrides❌Optional EIP-1559 gas settings for the L1 transaction.

Returns: DepositQuote


[!TIP] If summary.approvalsNeeded is non-empty (ERC-20), create() will automatically include those steps.

tryQuote(p) → Promise<{ ok: true; value: DepositQuote } | { ok: false; error }>

Result-style quote.

prepare(p: DepositParams) → Promise<DepositPlan<TransactionRequest>>

Build a plan (ordered steps + unsigned txs) without sending.

Returns: DepositPlan

const plan = await sdk.deposits.prepare({
  token,
  amount,
  to
});
/*
{
  route,
  summary: DepositQuote,
  steps: [
    { key: "approve:USDC", kind: "approve", tx: TransactionRequest },
    { key: "bridge", kind: "bridge", tx: TransactionRequest }
  ]
}
*/

tryPrepare(p) → Promise<{ ok: true; value: DepositPlan } | { ok: false; error }>

Result-style prepare.

create(p: DepositParams) → Promise<DepositHandle<TransactionRequest>>

Prepares and executes all required L1 steps. Returns a handle with the L1 tx hash and per-step hashes.

Returns: DepositHandle

const handle = await sdk.deposits.create({ token, amount, to });
/*
{
  kind: "deposit",
  l1TxHash: Hex,
  stepHashes: Record<string, Hex>,
  plan: DepositPlan
}
*/

[!WARNING] If any step reverts, create() throws a typed error. Prefer tryCreate() to avoid exceptions.

tryCreate(p) → Promise<{ ok: true; value: DepositHandle } | { ok: false; error }>

Result-style create.

status(handleOrHash) → Promise<DepositStatus>

Resolve current phase for a deposit. Accepts either a DepositHandle or a raw L1 tx hash.

PhaseMeaning
UNKNOWNNo L1 hash provided
L1_PENDINGL1 receipt not yet found
L1_INCLUDEDIncluded on L1; L2 hash not derivable yet
L2_PENDINGL2 hash known; waiting for L2 receipt
L2_EXECUTEDL2 receipt found with status === 1
L2_FAILEDL2 receipt found with status !== 1
const s = await sdk.deposits.status(handle);
// { phase, l1TxHash, l2TxHash? }

wait(handleOrHash, { for: 'l1' | 'l2' }) → Promise<TransactionReceipt | null>

Block until a checkpoint is reached.

  • { for: 'l1' } → L1 receipt (or null if no L1 hash)
  • { for: 'l2' } → L2 receipt after canonical execution (or null if no L1 hash)
const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' });
const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' });

tryWait(handleOrHash, opts) → Result<TransactionReceipt>

Result-style wait.


End-to-End Examples

ETH Deposit (Typical)

const handle = await sdk.deposits.create({
  token: ETH_ADDRESS,
  amount: parseEther('0.001'),
  to: account.address,
});

await sdk.deposits.wait(handle, { for: 'l2' });

ERC-20 Deposit (with Automatic Approvals)

const token = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // Example: USDC
const handle = await sdk.deposits.create({
  token,
  amount: 1_000_000n, // 1.0 USDC (6 decimals)
  to: account.address
});

const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' });

Utility Helpers

getL2TransactionHashFromLogs(logs) → Hex | null

Extracts the L2 transaction hash from L1 logs emitted by Bridgehub during deposit. Returns null if not found.

import { getL2TransactionHashFromLogs } from '@matterlabs/zksync-js/viem';

const l1Receipt = await client.l1.waitForTransactionReceipt({ hash: l1TxHash });
const l2TxHash = getL2TransactionHashFromLogs(l1Receipt.logs);

Types (Overview)

Deposit Params

interface DepositParams {
  token: Address;
  amount: bigint;
  to?: Address;
  refundRecipient?: Address;
  l2GasLimit?: bigint;
  gasPerPubdata?: bigint;
  operatorTip?: bigint;
  l1TxOverrides?: TxOverrides;
}

type TxOverrides = {
  gasLimit: bigint;
  maxFeePerGas: bigint;
  maxPriorityFeePerGas?: bigint | undefined;
}

Deposit Quote

type DepositRoute = 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase';

type L1DepositFeeParams = {
  gasLimit: bigint;
  maxFeePerGas: bigint;
  maxPriorityFeePerGas?: bigint;
  maxTotal: bigint;
};

type L2DepositFeeParams = {
  gasLimit: bigint;
  maxFeePerGas: bigint;
  maxPriorityFeePerGas?: bigint;
  total: bigint;
  baseCost: bigint;
  gasPerPubdata: bigint;
  operatorTip?: bigint;
};

type DepositFeeBreakdown = {
    token: `0x${string}`;
    maxTotal: bigint;
    mintValue?: bigint | undefined;
    l1?: L1DepositFeeParams | undefined;
    l2?: L2DepositFeeParams;
}

interface ApprovalNeed {
  token: Address;
  spender: Address;
  amount: bigint;
}

/** Quote */
interface DepositQuote {
  route: DepositRoute;
  approvalsNeeded: readonly ApprovalNeed[];
  amounts: {
    transfer: { token: Address; amount: bigint };
  };
  fees: DepositFeeBreakdown;
  /**
   * @deprecated Use `fees.components?.l2BaseCost` instead.
   * Will be removed in a future release.
   */
  baseCost?: bigint;
  /**
   * @deprecated Use `fees.components?.mintValue` instead.
   * Will be removed in a future release.
   */
  mintValue?: bigint;
}

Deposit Plan

interface PlanStep<Tx, Preview = undefined> {
  key: string;
  kind: string;
  description: string;
  /** Adapter-specific request (ethers TransactionRequest, viem WriteContractParameters, etc.) */
  tx: Tx;
  /** Optional compact, human-friendly view for logging/UI */
  preview?: Preview;
}

interface Plan<Tx, Route, Quote> {
  route: Route;
  summary: Quote;
  steps: Array<PlanStep<Tx>>;
}

/** Plan (Tx generic) */
type DepositPlan<Tx> = Plan<Tx, DepositRoute, DepositQuote>;

Deposit Waitable

interface Handle<TxHashMap extends Record<string, Hex>, Route, PlanT> {
  kind: 'deposit' | 'withdrawal';
  route?: Route;
  stepHashes: TxHashMap; // step key -> tx hash
  plan: PlanT;
}

/** Handle */
interface DepositHandle<Tx>
  extends Handle<Record<string, Hex>, DepositRoute, DepositPlan<Tx>> {
  kind: 'deposit';
  l1TxHash: Hex;
  l2ChainId?: number;
  l2TxHash?: Hex;
}

/** Waitable */
type DepositWaitable = Hex | { l1TxHash: Hex } | DepositHandle<unknown>;

Deposit Status

// Status and phases
type DepositPhase =
  | 'L1_PENDING'
  | 'L1_INCLUDED' // L1 included, L2 hash not derived yet
  | 'L2_PENDING' // we have L2 hash, but no receipt yet
  | 'L2_EXECUTED' // L2 receipt.status === 1
  | 'L2_FAILED' // L2 receipt.status === 0
  | 'UNKNOWN';

// Deposit Status
type DepositStatus = {
  phase: DepositPhase;
  l1TxHash: Hex;
  l2TxHash?: Hex;
};

[!TIP] Prefer the try* variants to avoid exceptions and work with structured result objects.


Notes & Pitfalls

  • ETH sentinel: Always use the canonical 0x…00 address when passing ETH as token.
  • Receipts timing: wait({ for: 'l2' }) resolves after canonical L2 execution — may take longer than L1 inclusion.
  • Gas hints: suggestedL2GasLimit and gasPerPubdata are informational; advanced users can override via the prepared plan.