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.

Parameters

NameTypeRequiredDescription
tokenAddressL1 token address. Use 0x…00 for ETH.
amountbigintAmount in wei to deposit.
toAddressL2 recipient address. Defaults to the signer’s address if omitted.
refundRecipientAddressOptional address on L1 to receive refunds for unspent gas.
l2GasLimitbigintOptional manual L2 gas limit override.
gasPerPubdatabigintOptional custom gas-per-pubdata value.
operatorTipbigintOptional operator tip (in wei) for priority execution.
l1TxOverridesEip1559GasOverridesOptional 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' });

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.