Skip to main content

At a glance

  • Factory: createEthersClient({ l1, l2, signer, overrides? }) → EthersClient
  • What it provides: cached core addresses, connected contracts, L2-bound ZKsync RPC (zks), and a signer force-bound to L1.
  • When to use: create this first; then pass into createEthersSdk(client).

Import

import { createEthersClient } from '@dutterbutter/zksync-sdk/ethers';

Quick start

import { JsonRpcProvider, Wallet } from 'ethers';
import { createEthersClient } from '@dutterbutter/zksync-sdk/ethers';

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

const client = createEthersClient({ l1, l2, signer });

// Resolve core addresses (cached)
const addrs = await client.ensureAddresses();

// Connected contracts
const { bridgehub, l1AssetRouter } = await client.contracts();
The signer is force-bound to the L1 provider to make L1 finalization flows work out of the box.

createEthersClient(args) → EthersClient

Returns: EthersClient

EthersClient interface

kind
'ethers'
Adapter discriminator.
l1
ethers.AbstractProvider
Public L1 provider.
l2
ethers.AbstractProvider
Public L2 (ZKsync) provider.
signer
ethers.Signer
Signer (bound to l1 for sends).
zks
ZksRpc
ZKsync-specific RPC surface bound to l2.

Methods

ensureAddresses() → Promise<ResolvedAddresses>

Resolve and cache core contract addresses from chain state (merges any overrides).
const a = await client.ensureAddresses();
/*
{
  bridgehub, l1AssetRouter, l1Nullifier, l1NativeTokenVault,
  l2AssetRouter, l2NativeTokenVault, l2BaseTokenSystem
}
*/

contracts() → Promise<{ ...contracts }>

Return connected ethers.Contract instances for all core contracts.
const c = await client.contracts();
const bh = c.bridgehub; // call bh.getAddress(), bh.interface, bh.functions.*, etc.

refresh(): void

Clear cached addresses/contracts. Subsequent calls re-resolve.
client.refresh();
await client.ensureAddresses();

baseToken(chainId: bigint) → Promise<Address>

Return the L1 base-token address for a given L2 chain via Bridgehub.baseToken(chainId).
const base = await client.baseToken(324n /* e.g., Era */);

Types

ResolvedAddresses

type ResolvedAddresses = {
  bridgehub: Address;
  l1AssetRouter: Address;
  l1Nullifier: Address;
  l1NativeTokenVault: Address;
  l2AssetRouter: Address;
  l2NativeTokenVault: Address;
  l2BaseTokenSystem: Address;
};

Notes & pitfalls

  • Provider roles: l1 is used for L1 lookups and finalization sends; l2 is used for ZKsync reads/RPC via zks.
  • Signer binding: The signer is connected to l1 to ensure L1 transactions (e.g., finalize) succeed without extra wiring.
  • Caching: ensureAddresses() and contracts() are cached. Call refresh() after network changes or when using new overrides.
  • Overrides: For forks or custom deployments, pass overrides at construction; they are merged with on-chain resolution.