Finalization Services

Helpers for building and executing L1 finalization of L2 withdrawals using the Viem adapter. These utilities fetch the required L2→L1 proof data, check readiness, and submit finalizeDeposit on the L1 Nullifier contract.

Use these services when you need fine-grained control (preflight simulations, custom gas, external orchestration). For the high-level path, see sdk.withdrawals.finalize(...).


At a Glance

  • Factory: createFinalizationServices(client) → FinalizationServices
  • Workflow: fetch paramsoptionally check statussimulate readinesssubmit finalize tx
  • Prereq: An initialized ViemClient with an L1 wallet (used to sign the L1 finalize tx).

Import & Setup

import { privateKeyToAccount } from 'viem/accounts';
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
import { createViemClient, createViemSdk, createFinalizationServices } from '@matterlabs/zksync-js/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); // optional
const svc = createFinalizationServices(client);

Minimal Usage Example

// 1) Build finalize params + discover the L1 Nullifier to call
const { params } = await svc.fetchFinalizeDepositParams(handle.l2TxHash);
const key: WithdrawalKey = {
  chainIdL2: params.chainId,
  l2BatchNumber: params.l2BatchNumber,
  l2MessageIndex: params.l2MessageIndex,
};
// 2) (Optional) check finalization
const already = await svc.isWithdrawalFinalized(key);
if (already) {
  console.log('Already finalized on L1');
} else {
  // 3) Dry-run on L1 to confirm readiness (no gas spent)
  const readiness = await svc.simulateFinalizeReadiness(params);

  if (readiness.kind === 'READY') {
    // 4) Submit finalize tx
    const { hash, wait } = await svc.finalizeDeposit(params);
    console.log('L1 finalize tx:', hash);
    const rcpt = await wait();
    console.log('Finalized in block:', rcpt.blockNumber);
  } else {
    console.warn('Not ready to finalize:', readiness);
  }
}

Tip: If you prefer the SDK to handle readiness checks automatically, call sdk.withdrawals.finalize(l2TxHash) instead.

API

fetchFinalizeDepositParams(l2TxHash) → Promise<{ params, nullifier }>

Builds the inputs required by Nullifier.finalizeDeposit for a given L2 withdrawal tx.

Parameters

NameTypeRequiredDescription
l2TxHashHexL2 withdrawal transaction hash.

Returns

FieldTypeDescription
paramsFinalizeDepositParamsCanonical finalize input (proof, indices, message).
nullifierAddressL1 Nullifier contract address to call.

isWithdrawalFinalized(key) → Promise<boolean>

Reads the Nullifier mapping to determine whether a withdrawal has already been finalized.

Parameters

NameTypeRequiredDescription
keyWithdrawalKeyUnique key for the withdrawal.

Returns: true if finalized; otherwise false.

simulateFinalizeReadiness(params, nullifier) → Promise<FinalizeReadiness>

Performs a static call on the L1 Nullifier to check whether finalizeDeposit would succeed now (no gas spent).

Parameters

NameTypeRequiredDescription
paramsFinalizeDepositParamsPrepared finalize input.
nullifierAddressL1 Nullifier address.

Returns: FinalizeReadiness (see Types).

finalizeDeposit(params, nullifier) → Promise<{ hash: string; wait: () => Promise<TransactionReceipt> }>

Sends the L1 finalize transaction to the Nullifier with the provided params.

Parameters

NameTypeRequiredDescription
paramsFinalizeDepositParamsPrepared finalize input.
nullifierAddressL1 Nullifier address.

Returns

FieldTypeDescription
hashstringSubmitted L1 transaction hash.
wait() => Promise<TransactionReceipt>Helper to await on-chain inclusion of the tx.

Warning: This call will revert if the withdrawal is not ready or invalid. Prefer simulateFinalizeReadiness or sdk.withdrawals.wait(..., { for: 'ready' }) first.

Status & Phases

If you are also using sdk.withdrawals.status(...), the phases align conceptually with readiness:

Withdrawal PhaseMeaningReadiness interpretation
L2_PENDINGL2 tx not in a block yetNot ready
L2_INCLUDEDL2 receipt is availableNot ready (proof not derivable yet)
PENDINGInclusion known; proof data not yet derivable/availableNOT_READY
READY_TO_FINALIZEProof posted; can be finalized on L1READY
FINALIZINGL1 finalize tx sent but not yet indexedBetween READY and FINALIZED
FINALIZEDWithdrawal finalized on L1FINALIZED
FINALIZE_FAILEDPrior L1 finalize revertedPossibly UNFINALIZABLE
UNKNOWNNo L2 hash or insufficient dataN/A

Types

type WithdrawalKey = {
  chainIdL2: bigint;
  l2BatchNumber: bigint;
  l2MessageIndex: bigint;
};

type WithdrawalPhase =
  | 'L2_PENDING' // tx not in an L2 block yet
  | 'L2_INCLUDED' // we have the L2 receipt
  | 'PENDING' // inclusion known; proof data not yet derivable/available
  | 'READY_TO_FINALIZE' // Ready to call finalize on L1
  | 'FINALIZING' // L1 tx sent but not picked up yet
  | 'FINALIZED' // L2-L1 tx finalized on L1
  | 'FINALIZE_FAILED' // prior L1 finalize reverted
  | 'UNKNOWN';

// Withdrawal Status
type WithdrawalStatus = {
  phase: WithdrawalPhase;
  l2TxHash: Hex;
  l1FinalizeTxHash?: Hex;
  key?: WithdrawalKey;
};

interface FinalizeDepositParams {
  chainId: bigint;
  l2BatchNumber: bigint;
  l2MessageIndex: bigint;
  l2Sender: Address;
  l2TxNumberInBatch: number;
  message: Hex;
  merkleProof: Hex[];
}

// Finalization readiness states
// Used for `status()`
type FinalizeReadiness =
  | { kind: 'READY' }
  | { kind: 'FINALIZED' }
  | {
      kind: 'NOT_READY';
      // temporary, retry later
      reason: 'paused' | 'batch-not-executed' | 'root-missing' | 'unknown';
      detail?: string;
    }
  | {
      kind: 'UNFINALIZABLE';
      // permanent, won’t become ready
      reason: 'message-invalid' | 'invalid-chain' | 'settlement-layer' | 'unsupported';
      detail?: string;
    };

interface FinalizationEstimate {
  gasLimit: bigint;
  maxFeePerGas: bigint;
  maxPriorityFeePerGas: bigint;
}

interface FinalizationServices {
  /**
   * Build finalizeDeposit params.
   */
  fetchFinalizeDepositParams(
    l2TxHash: Hex,
  ): Promise<{ params: FinalizeDepositParams; nullifier: Address }>;

  /**
   * Read the Nullifier mapping to check finalization status.
   */
  isWithdrawalFinalized(key: WithdrawalKey): Promise<boolean>;

  /**
   * Simulate finalizeDeposit on L1 Nullifier to check readiness.
   */
  simulateFinalizeReadiness(params: FinalizeDepositParams): Promise<FinalizeReadiness>;

  /**
   * Estimate gas & fees for finalizeDeposit on L1 Nullifier.
   */
  estimateFinalization(params: FinalizeDepositParams): Promise<FinalizationEstimate>;

  /**
   * Call finalizeDeposit on L1 Nullifier.
   */
  finalizeDeposit(
    params: FinalizeDepositParams,
  ): Promise<{ hash: string; wait: () => Promise<TransactionReceipt> }>;
}

Notes & Pitfalls

  • Anyone can finalize: The withdrawer, a relayer, or your backend—finalization is permissionless.
  • Delay is expected: Proof generation/posting introduce lag between L2 inclusion and readiness.
  • Gas: Finalization is an L1 transaction; ensure the L1 wallet has ETH for gas.
  • Error surface: Underlying calls can throw typed errors (STATE, RPC, VERIFICATION). Check readiness to avoid avoidable failures.

Cross-References