Skip to main content

At a glance

  • Factory: createViemClient({ l1, l2, l1Wallet, l2Wallet?, overrides? }) → ViemClient
  • What it provides: cached core addresses, typed contracts, convenience wallet access (L2 wallet derivation), and ZKsync RPC bound to l2.
  • When to use: create this first; then pass into createViemSdk(client).

Import

import { createViemClient } from '@dutterbutter/zksync-sdk/viem';

Quick start

import { createPublicClient, createWalletClient, http } from "viem";

// Public clients (reads)
const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) });
const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) });

// Wallet clients (writes)
const l1Wallet = createWalletClient({
  account: /* your L1 Account */,
  transport: http(process.env.ETH_RPC!),
});

// Optional dedicated L2 wallet. Required for L2 sends (withdrawals).
const l2Wallet = createWalletClient({
  account: /* can be same key as L1 */,
  transport: http(process.env.ZKSYNC_RPC!),
});

const client = createViemClient({ l1, l2, l1Wallet, l2Wallet });

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

// Typed contracts (viem getContract)
const { bridgehub, l1AssetRouter } = await client.contracts();
l1Wallet.account is required. If you omit l2Wallet, use client.getL2Wallet(); it lazily reuses the L1 account over the L2 transport.

createViemClient(args) → ViemClient

Returns: ViemClient

ViemClient interface

kind
'viem'
Adapter discriminator.
l1
viem.PublicClient
Public L1 client.
l2
viem.PublicClient
Public L2 (ZKsync) client.
l1Wallet
viem.WalletClient<T, C, A>
Wallet bound to L1 (carries default account).
l2Wallet
viem.WalletClient<T, C, A> | undefined
Optional pre-supplied L2 wallet.
account
viem.Account
Default account (from l1Wallet).
zks
ZksRpc
ZKsync-specific RPC 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 typed viem contracts (getContract) connected to the current clients.
const c = await client.contracts();
const bh = c.bridgehub; // bh.read.*, bh.write.*, bh.simulate.*

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 /* example L2 chain id */);

getL2Wallet() → viem.WalletClient

Return the L2 wallet. If not provided at construction, lazily creates one from the same account as l1Wallet over the L2 transport.
const w = client.getL2Wallet(); // ensures L2 writes are possible

Types

ResolvedAddresses

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

Notes & pitfalls

  • Wallet placement: Deposits sign on L1; withdrawals sign on L2; finalization signs on L1 (via SDK).
  • Caching: ensureAddresses() and contracts() are cached. Use refresh() after network/override changes.
  • Overrides: For forks or custom deployments, pass overrides at construction; they’ll be merged with on-chain lookups.
  • Error surface: Methods may throw typed errors; use the SDK’s try* variants (on resources) if you prefer result objects.