> ## Documentation Index
> Fetch the complete documentation index at: https://zksync-sdk.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Withdrawals

> L2 → L1 withdrawals for ETH and ERC-20 with quote, prepare, create, status, wait, and finalize helpers (Viem adapter).

## At a glance

* **Resource:** `sdk.withdrawals`
* **Typical flow:** `quote → create → wait({ for: 'l2' }) → wait({ for: 'ready' }) → finalize`
* **Auto-routing:** ETH vs ERC-20 and base-token vs non-base handled internally
* **Error style:** Throwing methods (`quote`, `prepare`, `create`, `status`, `wait`, `finalize`) + result variants (`tryQuote`, `tryPrepare`, `tryCreate`, `tryWait`, `tryFinalize`)

## Import

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
import {
  createPublicClient,
  createWalletClient,
  http,
  parseEther,
  type Account,
  type Chain,
  type Transport,
  type WalletClient,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem';

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),
});

// --- 2. Initialize the SDK ---
const client = createViemClient({ l1, l2, l1Wallet });
const sdk = createViemSdk(client);
// sdk.withdrawals → WithdrawalsResource
```

## Quick start

Withdraw **0.1 ETH** from L2 → L1 and finalize on L1:

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const handle = await sdk.withdrawals.create({
  token: ETH_ADDRESS, // ETH sentinel supported
  amount: parseEther('0.1'),
  to: await signer.getAddress(), // L1 recipient
});

// 1) L2 inclusion (adds l2ToL1Logs if available)
await sdk.withdrawals.wait(handle, { for: 'l2' });

// 2) Wait until finalizable (no side effects)
await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000 });

// 3) Finalize on L1 (no-op if already finalized)
const { status, receipt: l1Receipt } = await sdk.withdrawals.finalize(handle.l2TxHash);
```

<Info>
  Withdrawals are two-phase: inclusion on **L2**, then **finalization on L1**. You can call <code>finalize</code> directly; it will throw if not yet ready. Prefer <code>`wait(..., { for: 'ready' })`</code> to avoid that.
</Info>

## Route selection (automatic)

* `eth-base` — Base token is **ETH** on L2
* `eth-nonbase` — Base token is **not ETH** on L2
* `erc20-nonbase` — Withdrawing an ERC-20 that is **not** the base token

You **do not** pass a route; it’s derived from network metadata + `token`.

## Method reference

### `quote(p: WithdrawParams) → Promise<WithdrawQuote>`

Estimate the operation (route, approvals, gas hints). Does **not** send txs.

<ParamField name="p.token" type="Address" required>
  L2 token (ETH handled by the SDK; ETH sentinel supported).
</ParamField>

<ParamField name="p.amount" type="bigint" required>
  Amount in wei.
</ParamField>

<ParamField name="p.to" type="Address" required>
  L1 recipient.
</ParamField>

**Returns:** `WithdrawQuote`

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const q = await sdk.withdrawals.quote({ token, amount, to });
/*
{
  route: "eth-base" | "eth-nonbase" | "erc20-nonbase",
  approvalsNeeded: [{ token, spender, amount }],
  suggestedL2GasLimit?: bigint
}
*/
```

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

Result-style `quote`.

### `prepare(p: WithdrawParams) → Promise<WithdrawPlan<TransactionRequest>>`

Builds the plan (ordered L2 steps + unsigned txs) without sending.

**Returns:** `WithdrawPlan`

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const plan = await sdk.withdrawals.prepare({ token, amount, to });
/*
{
  route,
  summary: WithdrawQuote,
  steps: [
    { key, kind, tx: TransactionRequest },
    // …
  ]
}
*/
```

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

Result-style `prepare`.

### `create(p: WithdrawParams) → Promise<WithdrawHandle<TransactionRequest>>`

Prepares and **executes** required **L2** steps. Returns a handle with the **L2 tx hash**.

**Returns:** `WithdrawHandle`

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const handle = await sdk.withdrawals.create({ token, amount, to });
/*
{
  kind: "withdrawal",
  l2TxHash: Hex,
  stepHashes: Record<string, Hex>,
  plan: WithdrawPlan
}
*/
```

<Warning>
  If any L2 step reverts, <code>create</code> throws a typed error. Prefer <code>tryCreate</code>{' '}
  for a result object.
</Warning>

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

Result-style `create`.

### `status(handleOrHash) → Promise<WithdrawalStatus>`

Report current phase for a withdrawal. Accepts the `WithdrawHandle` from `create` **or** a raw **L2 tx hash**.

**Phases**

* `UNKNOWN` — no L2 hash provided
* `L2_PENDING` — L2 receipt missing
* `PENDING` — included on L2 but not yet finalizable
* `READY_TO_FINALIZE` — can be finalized on L1 now
* `FINALIZED` — already finalized on L1

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const s = await sdk.withdrawals.status(handle);
// { phase, l2TxHash, key? }
```

### `wait(handleOrHash, { for: 'l2' | 'ready' | 'finalized', pollMs?, timeoutMs? })`

Block until a target is reached.

* `{ for: 'l2' }` → resolves **L2 receipt** (`TransactionReceiptZKsyncOS`) or `null`
* `{ for: 'ready' }` → resolves `null` when finalizable
* `{ for: 'finalized' }` → resolves **L1 receipt** (if found) or `null`

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const l2Rcpt = await sdk.withdrawals.wait(handle, { for: 'l2' });
await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000, timeoutMs: 15 * 60_000 });
const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', pollMs: 7000 });
```

<Tip>
  Default polling is 5500 ms (min 1000 ms). Use <code>timeoutMs</code> for long windows.
</Tip>

### `tryWait(handleOrHash, opts) → Result<TransactionReceipt | null>`

Result-style `wait`.

### `finalize(l2TxHash: Hex) → Promise<{ status: WithdrawalStatus; receipt?: TransactionReceipt }>`

Sends the **L1 finalize** transaction **if** ready. If already finalized, returns the status without sending.

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const { status, receipt } = await sdk.withdrawals.finalize(handle.l2TxHash);
if (status.phase === 'FINALIZED') {
  console.log('L1 tx:', receipt?.transactionHash);
}
```

<Info>
  If not ready, <code>finalize</code> throws a typed <code>STATE</code> error. Use <code>status(...)</code> or <code>`wait(..., { for: 'ready' })`</code> first to avoid throws.
</Info>

### `tryFinalize(l2TxHash) → Promise<{ ok: true; value: { status: WithdrawalStatus; receipt?: TransactionReceipt } } | { ok: false; error }>`

Result-style `finalize`.

## End-to-end examples

### Minimal happy path

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
const handle = await sdk.withdrawals.create({ token, amount, to });

// L2 inclusion
await sdk.withdrawals.wait(handle, { for: 'l2' });

// Option A: finalize immediately (will throw if not ready)
await sdk.withdrawals.finalize(handle.l2TxHash);

// Option B: wait for readiness, then finalize
await sdk.withdrawals.wait(handle, { for: 'ready' });
await sdk.withdrawals.finalize(handle.l2TxHash);
```

## Types (overview)

```ts theme={"theme":{"light":"vitesse-light","dark":"tokyo-night"}}
type WithdrawParams = {
  token: Address; // L2 token (ETH sentinel supported)
  amount: bigint; // wei
  to: Address; // L1 recipient
};

type WithdrawQuote = {
  route: 'eth-base' | 'eth-nonbase' | 'erc20-nonbase';
  approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>;
  suggestedL2GasLimit?: bigint;
};

type WithdrawPlan<TTx = TransactionRequest> = {
  route: WithdrawQuote['route'];
  summary: WithdrawQuote;
  steps: Array<{ key: string; kind: string; tx: TTx }>;
};

type WithdrawHandle<TTx = TransactionRequest> = {
  kind: 'withdrawal';
  l2TxHash: Hex;
  stepHashes: Record<string, Hex>;
  plan: WithdrawPlan<TTx>;
};

type WithdrawalStatus =
  | { phase: 'UNKNOWN'; l2TxHash: Hex }
  | { phase: 'L2_PENDING'; l2TxHash: Hex }
  | { phase: 'PENDING'; l2TxHash: Hex; key?: unknown }
  | { phase: 'READY_TO_FINALIZE'; l2TxHash: Hex; key: unknown }
  | { phase: 'FINALIZED'; l2TxHash: Hex; key: unknown };

// L2 receipt augmentation returned by wait({ for: 'l2' })
type TransactionReceiptZKsyncOS = TransactionReceipt & { l2ToL1Logs?: Array<unknown> };
```

## Notes & pitfalls

* **Two chains, two receipts:** inclusion on **L2** and finalization on **L1** are independent events.
* **Polling strategy:** for production UIs, prefer <code>`wait({ for: 'ready' })`</code> then <code>finalize</code>; it avoids premature finalize calls.
* **Approvals:** if withdrawing ERC-20 requires approvals, `create` will include those steps automatically.
