Finalization Services

Helpers for building and executing L1 finalization of L2 withdrawals using the Ethers 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 (e.g., 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 EthersClient (bound to L1 for signing).

Import & Setup

import { JsonRpcProvider, Wallet, parseEther } from 'ethers';
import { createEthersClient, createEthersSdk, createFinalizationServices } from '@matterlabs/zksync-js/ethers';

const l1 = new JsonRpcProvider(process.env.L1_RPC!);
const l2 = new JsonRpcProvider(process.env.L2_RPC!);
const signer = new Wallet(process.env.PRIVATE_KEY!, l1);

const client = createEthersClient({ l1, l2, signer });
const sdk = createEthersSdk(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 a finalizeDeposit would succeed now (no gas spent).

Parameters

NameTypeRequiredDescription
paramsFinalizeDepositParamsPrepared finalize input.
nullifierAddressL1 Nullifier address.

Returns: FinalizeReadiness

Readiness states (see Types) include:

  • { kind: 'READY' }
  • { kind: 'FINALIZED' }
  • { kind: 'NOT_READY', reason, detail? } (temporary)
  • { kind: 'UNFINALIZABLE', reason, detail? } (permanent)

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 method will revert if the withdrawal is not ready or invalid. Prefer calling simulateFinalizeReadiness or using 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 revertedLikely UNFINALIZABLE until state changes
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: It’s permissionless; your backend or a third-party relayer can call finalizeDeposit.
  • Delay is normal: Proof availability and posting introduce lag between L2 inclusion and readiness.
  • Gas/accounting: Since finalizeDeposit is an L1 tx, ensure the L1 signer has ETH for gas.
  • Error model: Underlying calls may throw typed errors (e.g., STATE, RPC, VERIFICATION). Use readiness checks to avoid avoidable failures.

Cross-References