Batch Payroll (1-to-N)
This guide shows how to send private payments to multiple recipients (1-to-N) using a batch transfer pattern. Ideal for payroll, contractor payments, and vendor disbursements.
Overview
Section titled “Overview”The batch payroll pattern extends the single payment flow to handle multiple payees efficiently:
- One wrap transaction for the total amount
- One batch transfer to all deposit addresses
- Individual redemptions per payee (can be batched with EIP-5792)
This reduces gas costs and simplifies the sender’s workflow while maintaining privacy for each recipient.
Key Differences from Single Payments
Section titled “Key Differences from Single Payments”| Aspect | Single Payment | Batch Payroll |
|---|---|---|
| Payment keys | 1 | N (one per payee) |
| Wrap transactions | 1 | 1 |
| Transfer transactions | 1 | 1 (via batch proxy) |
| Redemption transactions | 1 | N (or batched) |
Implementation
Section titled “Implementation”First, set up the environment and configuration:
import { import Abi
Abi, import Domain
Domain, import IbcCoreRegistry
IbcCoreRegistry } from "@unionlabs/payments";import { import Attestor
Attestor, import EvmPublicClient
EvmPublicClient, import EvmWalletClient
EvmWalletClient, import Payment
Payment, import Prover
Prover,} from "@unionlabs/payments/promises";import * as import Viem
Viem from "viem";import * as import ViemAccounts
ViemAccounts from "viem/accounts";import * as import ViemChains
ViemChains from "viem/chains";
const const BASE_RPC: "https://base-mainnet.g.alchemy.com/v2/<redacted>"
BASE_RPC = "https://base-mainnet.g.alchemy.com/v2/<redacted>";
const const ATTESTOR_KEY: string
ATTESTOR_KEY = var process: NodeJS.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
The process.env property returns an object containing the user environment.
See environ(7).
An example of this object looks like:
{ TERM: 'xterm-256color', SHELL: '/usr/local/bin/bash', USER: 'maciej', PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin', PWD: '/Users/maciej', EDITOR: 'vim', SHLVL: '1', HOME: '/Users/maciej', LOGNAME: 'maciej', _: '/usr/local/bin/node'}
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Terminal window node -e 'process.env.foo = "bar"' && echo $foo
While the following will:
import { env } from 'node:process';
env.foo = 'bar';console.log(env.foo);
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from 'node:process';
env.test = null;console.log(env.test);// => 'null'env.test = undefined;console.log(env.test);// => 'undefined'
Use delete to delete a property from process.env.
import { env } from 'node:process';
env.TEST = 1;delete env.TEST;console.log(env.TEST);// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from 'node:process';
env.TEST = 1;console.log(env.test);// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
env.string | undefined
ATTESTOR_KEY ?? "6af6f8068d38ebf6666b8db98f9b8b42959ab62646764bc290e663f7abb49eea";
const const USDC_ADDRESS: Domain.Erc20Address
USDC_ADDRESS = import Domain
Domain.const Erc20Address: Brand<in out K extends string | symbol>.Constructor(args: `0x${string}`) => Domain.Erc20Address
Constructs a branded type from a value of type A, throwing an error if
the provided A is not valid.
Erc20Address( "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",);
const const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID = import Domain
Domain.const UniversalChainId: brand<filter<typeof String$>, "UniversalChainId">
UniversalChainId.BrandSchema<string & Brand<"UniversalChainId">, string, never>.make(a: string, options?: MakeOptions): string & Brand<"UniversalChainId">
make("base.8453");const const CLIENT_ID: 5
CLIENT_ID = 5;
/** * Batch proxy contract for multi-recipient transfers. * This example uses Request Finance's ERC20BatchPayments contract. * You can deploy your own or use any contract with a compatible interface. * @see https://github.com/RequestNetwork/requestNetwork */const const BATCH_PROXY_ADDRESS: `0x${string}`
Batch proxy contract for multi-recipient transfers.
This example uses Request Finance's ERC20BatchPayments contract.
You can deploy your own or use any contract with a compatible interface.
BATCH_PROXY_ADDRESS: `0x${string}` = "0x0DD57FFe83a53bCbd657e234B16A3e74fEDb8fBA";const const BATCH_PROXY_ABI: readonly [{ readonly inputs: readonly [{ readonly name: "_tokenAddress"; readonly type: "address"; }, { readonly name: "_recipients"; readonly type: "address[]"; }, { readonly name: "_amounts"; readonly type: "uint256[]"; }, { readonly name: "_paymentReferences"; readonly type: "bytes[]"; }, { readonly name: "_feeAmounts"; readonly type: "uint256[]"; }, { readonly name: "_feeAddress"; readonly type: "address"; }]; readonly name: "batchERC20PaymentsWithReference"; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; readonly type: "function";}]
BATCH_PROXY_ABI = [ { inputs: readonly [{ readonly name: "_tokenAddress"; readonly type: "address";}, { readonly name: "_recipients"; readonly type: "address[]";}, { readonly name: "_amounts"; readonly type: "uint256[]";}, { readonly name: "_paymentReferences"; readonly type: "bytes[]";}, { readonly name: "_feeAmounts"; readonly type: "uint256[]";}, { readonly name: "_feeAddress"; readonly type: "address";}]
inputs: [ { name: "_tokenAddress"
name: "_tokenAddress", type: "address"
type: "address" }, { name: "_recipients"
name: "_recipients", type: "address[]"
type: "address[]" }, { name: "_amounts"
name: "_amounts", type: "uint256[]"
type: "uint256[]" }, { name: "_paymentReferences"
name: "_paymentReferences", type: "bytes[]"
type: "bytes[]" }, { name: "_feeAmounts"
name: "_feeAmounts", type: "uint256[]"
type: "uint256[]" }, { name: "_feeAddress"
name: "_feeAddress", type: "address"
type: "address" }, ], name: "batchERC20PaymentsWithReference"
name: "batchERC20PaymentsWithReference", outputs: readonly []
outputs: [], stateMutability: "nonpayable"
stateMutability: "nonpayable", type: "function"
type: "function", },] as type const = readonly [{ readonly inputs: readonly [{ readonly name: "_tokenAddress"; readonly type: "address"; }, { readonly name: "_recipients"; readonly type: "address[]"; }, { readonly name: "_amounts"; readonly type: "uint256[]"; }, { readonly name: "_paymentReferences"; readonly type: "bytes[]"; }, { readonly name: "_feeAmounts"; readonly type: "uint256[]"; }, { readonly name: "_feeAddress"; readonly type: "address"; }]; readonly name: "batchERC20PaymentsWithReference"; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; readonly type: "function";}]
const;
const const privateKey: `0x${string}`
privateKey = var process: NodeJS.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
The process.env property returns an object containing the user environment.
See environ(7).
An example of this object looks like:
{ TERM: 'xterm-256color', SHELL: '/usr/local/bin/bash', USER: 'maciej', PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin', PWD: '/Users/maciej', EDITOR: 'vim', SHLVL: '1', HOME: '/Users/maciej', LOGNAME: 'maciej', _: '/usr/local/bin/node'}
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Terminal window node -e 'process.env.foo = "bar"' && echo $foo
While the following will:
import { env } from 'node:process';
env.foo = 'bar';console.log(env.foo);
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from 'node:process';
env.test = null;console.log(env.test);// => 'null'env.test = undefined;console.log(env.test);// => 'undefined'
Use delete to delete a property from process.env.
import { env } from 'node:process';
env.TEST = 1;delete env.TEST;console.log(env.TEST);// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from 'node:process';
env.TEST = 1;console.log(env.test);// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
env.string | undefined
PRIVATE_KEY as `0x${string}`;if (!const privateKey: `0x${string}`
privateKey) throw new var Error: ErrorConstructornew (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("Please provide a PRIVATE_KEY as an env var.");
const const walletAddress: `0x${string}`
walletAddress = import ViemAccounts
ViemAccounts.function privateKeyToAccount(privateKey: Viem.Hex, options?: ViemAccounts.PrivateKeyToAccountOptions): Viem.PrivateKeyAccountexport privateKeyToAccount
privateKeyToAccount(const privateKey: `0x${string}`
privateKey).address: `0x${string}`
address;Create SDK clients from viem:
const const viemPublicClient: { account: undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; request: Viem.EIP1193RequestFn<Viem.PublicRpcSchema>; ... 59 more ...; extend: <const client extends { ...; } & Viem.ExactPartial<...>>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}
viemPublicClient = import Viem
Viem.createPublicClient<Viem.HttpTransport<undefined, false>, { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}, undefined, []>(parameters: { ...;}): { ...;}export createPublicClient
Creates a Public Client with a given Transport configured for a Chain.
A Public Client is an interface to "public" JSON-RPC API methods such as retrieving block numbers, transactions, reading from smart contracts, etc through Public Actions.
createPublicClient({ chain?: Viem.Chain | { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };} | undefined
Chain for the client.
chain: import ViemChains
ViemChains.const base: { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}export base
base, transport: Viem.HttpTransport<undefined, false>
The RPC transport
transport: import Viem
Viem.http<undefined, false>(url?: string | undefined, config?: Viem.HttpTransportConfig<undefined, false> | undefined): Viem.HttpTransport<undefined, false>export http
http(const BASE_RPC: "https://base-mainnet.g.alchemy.com/v2/<redacted>"
BASE_RPC),}) as import Viem
Viem.type PublicClient<transport extends Transport = Transport, chain extends Chain | undefined = Chain | undefined, accountOrAddress extends Account | undefined = undefined, rpcSchema extends RpcSchema | undefined = undefined> = { account: accountOrAddress; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: chain; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; request: Viem.EIP1193RequestFn<...>; ... 59 more ...; extend: <const client extends { ...; } & Viem.ExactPartial<...>>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}export PublicClient
PublicClient;
const const viemWalletClient: { account: Viem.Account | undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; ... 31 more ...; extend: <const client extends { ...; } & Viem.ExactPartial<...>>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}
viemWalletClient = import Viem
Viem.createWalletClient<Viem.HttpTransport<undefined, false>, { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}, { ...;}, []>(parameters: { ...;}): { ...;}export createWalletClient
Creates a Wallet Client with a given Transport configured for a Chain.
A Wallet Client is an interface to interact with Ethereum Account(s) and provides the ability to retrieve accounts, execute transactions, sign messages, etc. through Wallet Actions.
The Wallet Client supports signing over:
- JSON-RPC Accounts (e.g. Browser Extension Wallets, WalletConnect, etc).
- Local Accounts (e.g. private key/mnemonic wallets).
createWalletClient({ account?: `0x${string}` | { address: Viem.Address; nonceManager?: Viem.NonceManager | undefined; sign: (parameters: { hash: Viem.Hash; }) => Promise<Viem.Hex>; signAuthorization: (parameters: Viem.AuthorizationRequest) => Promise<ViemAccounts.SignAuthorizationReturnType>; signMessage: ({ message }: { message: Viem.SignableMessage; }) => Promise<Viem.Hex>; signTransaction: <serializer extends Viem.SerializeTransactionFn<Viem.TransactionSerializable> = Viem.SerializeTransactionFn<Viem.TransactionSerializable>, transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]>(transaction: transaction, options?: { serializer?: serializer | undefined; } | undefined) => Promise<Viem.Hex>; signTypedData: <const typedData extends Viem.TypedData | Record<string, unknown>, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData>(parameters: Viem.TypedDataDefinition<typedData, primaryType>) => Promise<Viem.Hex>; publicKey: Viem.Hex; source: "privateKey"; type: "local";} | Viem.Account | undefined
The Account to use for the Client. This will be used for Actions that require an account as an argument.
account: import ViemAccounts
ViemAccounts.function privateKeyToAccount(privateKey: Viem.Hex, options?: ViemAccounts.PrivateKeyToAccountOptions): Viem.PrivateKeyAccountexport privateKeyToAccount
privateKeyToAccount(const privateKey: `0x${string}`
privateKey), chain?: Viem.Chain | { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };} | undefined
Chain for the client.
chain: import ViemChains
ViemChains.const base: { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}export base
base, transport: Viem.HttpTransport<undefined, false>
The RPC transport
transport: import Viem
Viem.http<undefined, false>(url?: string | undefined, config?: Viem.HttpTransportConfig<undefined, false> | undefined): Viem.HttpTransport<undefined, false>export http
http(const BASE_RPC: "https://base-mainnet.g.alchemy.com/v2/<redacted>"
BASE_RPC),}) as import Viem
Viem.type WalletClient<transport extends Transport = Transport, chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, rpcSchema extends RpcSchema | undefined = undefined> = { account: account; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: chain; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; request: Viem.EIP1193RequestFn<(rpcSchema extends Viem.RpcSchema ? [...] : Viem.WalletRpcSchema) extends undefined ? [...] : rpcSchema extends Viem.RpcSchema ? [...] : Viem.WalletRpcSchema>; ... 30 more ...; extend: <const client extends { ...; } & Viem.ExactPartial<...>>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}export WalletClient
WalletClient;
const const publicClient: EvmPublicClient.EvmPublicClient
publicClient = await import EvmPublicClient
EvmPublicClient.const fromViem: (config: { account: undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; request: Viem.EIP1193RequestFn<...>; ... 59 more ...; extend: <client>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}) => Promise<...>
fromViem(const viemPublicClient: { account: undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; request: Viem.EIP1193RequestFn<Viem.PublicRpcSchema>; ... 59 more ...; extend: <const client extends { ...; } & Viem.ExactPartial<...>>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}
viemPublicClient);const const walletClient: EvmWalletClient.EvmWalletClient
walletClient = await import EvmWalletClient
EvmWalletClient.const fromViem: (config: { account: Viem.Account | undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; ... 31 more ...; extend: <client>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}) => Promise<...>
fromViem(const viemWalletClient: { account: Viem.Account | undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; ... 31 more ...; extend: <const client extends { ...; } & Viem.ExactPartial<...>>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}
viemWalletClient);Initialize the Attestor and Prover services:
const const attestor: Attestor.Attestor
attestor = await import Attestor
Attestor.const make: (options: Parameters<(options: { readonly apiKey: string; readonly baseUrl?: string | URL | undefined;}) => Layer<Attestor, SystemError, never>>[0]) => Promise<Attestor.Attestor>
make({ apiKey: string
apiKey: const ATTESTOR_KEY: string
ATTESTOR_KEY });const const prover: Prover.Prover
prover = await import Prover
Prover.const make: (options: Parameters<(options: { readonly proverUrl?: string | URL | undefined;}) => Layer<Prover, SystemError, never>>[0]) => Promise<Prover.Prover>
make({});Define your payees with their wallet addresses and amounts. USDC uses 6 decimals, so 1_000_000n equals 1 USDC.
const const payees: { address: `0x${string}`; amount: bigint;}[]
payees: interface Array<T>
Array<{ address: `0x${string}`
address: `0x${string}`; amount: bigint
amount: bigint }> = [ { address: `0x${string}`
address: "0x1111111111111111111111111111111111111111", amount: bigint
amount: 1_000_000n }, { address: `0x${string}`
address: "0x2222222222222222222222222222222222222222", amount: bigint
amount: 2_500_000n }, { address: `0x${string}`
address: "0x3333333333333333333333333333333333333333", amount: bigint
amount: 1_750_000n },];
const const totalAmount: bigint
totalAmount = const payees: { address: `0x${string}`; amount: bigint;}[]
payees.Array<{ address: `0x${string}`; amount: bigint; }>.reduce<bigint>(callbackfn: (previousValue: bigint, currentValue: { address: `0x${string}`; amount: bigint;}, currentIndex: number, array: { address: `0x${string}`; amount: bigint;}[]) => bigint, initialValue: bigint): bigint (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
reduce((sum: bigint
sum, p: { address: `0x${string}`; amount: bigint;}
p) => sum: bigint
sum + p: { address: `0x${string}`; amount: bigint;}
p.amount: bigint
amount, 0n);Generate payment keys and deposit addresses for all payees at once using Payment.createBatchPayment():
const const payments: CreateBatchPaymentResult[]
payments = await import Payment
Payment.const createBatchPayment: (options: { payees: ReadonlyArray<CreateBatchPaymentPayee>; destinationChainId: Domain.UniversalChainId;}) => Promise<CreateBatchPaymentResult[]>
createBatchPayment({ payees: readonly CreateBatchPaymentPayee[]
payees: const payees: { address: `0x${string}`; amount: bigint;}[]
payees.Array<{ address: `0x${string}`; amount: bigint; }>.map<{ address: Domain.Erc20Address; amount: bigint;}>(callbackfn: (value: { address: `0x${string}`; amount: bigint;}, index: number, array: { address: `0x${string}`; amount: bigint;}[]) => { address: Domain.Erc20Address; amount: bigint;}, thisArg?: any): { address: Domain.Erc20Address; amount: bigint;}[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map((p: { address: `0x${string}`; amount: bigint;}
p) => ({ address: Domain.Erc20Address
address: import Domain
Domain.const Erc20Address: Brand<in out K extends string | symbol>.Constructor(args: `0x${string}`) => Domain.Erc20Address
Constructs a branded type from a value of type A, throwing an error if
the provided A is not valid.
Erc20Address(p: { address: `0x${string}`; amount: bigint;}
p.address: `0x${string}`
address), amount: bigint
amount: p: { address: `0x${string}`; amount: bigint;}
p.amount: bigint
amount, })), destinationChainId: string & Brand<"UniversalChainId">
destinationChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID,});Prepare the ERC20 approval and wrap transactions for the total amount across all payees:
const [const approveErc20: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
approveErc20, const wrap: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
wrap] = await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.WalletClient.prepareWrap: (args: WalletClient.Wrap) => Promise<Domain.PreparedWrapRequests>
prepareWrap({ universalChainId: string & Brand<"UniversalChainId">
universalChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID, srcErc20Address: Domain.Erc20Address
srcErc20Address: const USDC_ADDRESS: Domain.Erc20Address
USDC_ADDRESS, amount: bigint
amount: const totalAmount: bigint
totalAmount,});This is the key step that differs from single payments. We use a batch proxy contract to transfer zUSDC to all deposit addresses in one transaction.
The batch combines 4 operations atomically:
- Approve zAsset contract to spend USDC
- Wrap USDC to zUSDC
- Approve batch proxy to spend zUSDC
- Transfer zUSDC to all deposit addresses
const const zAssetAddress: Domain.ZAssetAddress
zAssetAddress = const payments: CreateBatchPaymentResult[]
payments[0].CreateBatchPaymentResult.depositAddress: DepositAddress
Deposit address containing the zAsset address for transfers
depositAddress.DepositAddress.zAssetAddress: Domain.ZAssetAddress
zAssetAddress;
const const approveBatchProxy: { to: Domain.ZAssetAddress; abi: readonly [{ readonly inputs: readonly [{ readonly name: "spender"; readonly type: "address"; }, { readonly name: "amount"; readonly type: "uint256"; }]; readonly name: "approve"; readonly outputs: readonly [{ readonly name: ""; readonly type: "bool"; }]; readonly stateMutability: "nonpayable"; readonly type: "function"; }]; functionName: string; args: (bigint | `0x${string}`)[];}
approveBatchProxy = { to: Domain.ZAssetAddress
to: const zAssetAddress: Domain.ZAssetAddress
zAssetAddress, abi: readonly [{ readonly inputs: readonly [{ readonly name: "spender"; readonly type: "address"; }, { readonly name: "amount"; readonly type: "uint256"; }]; readonly name: "approve"; readonly outputs: readonly [{ readonly name: ""; readonly type: "bool"; }]; readonly stateMutability: "nonpayable"; readonly type: "function";}]
abi: import Abi
Abi.const ERC20_ABI: readonly [{ readonly inputs: readonly [{ readonly name: "spender"; readonly type: "address"; }, { readonly name: "amount"; readonly type: "uint256"; }]; readonly name: "approve"; readonly outputs: readonly [{ readonly name: ""; readonly type: "bool"; }]; readonly stateMutability: "nonpayable"; readonly type: "function";}]
ERC20_ABI, functionName: string
functionName: "approve", args: (bigint | `0x${string}`)[]
args: [const BATCH_PROXY_ADDRESS: `0x${string}`
Batch proxy contract for multi-recipient transfers.
This example uses Request Finance's ERC20BatchPayments contract.
You can deploy your own or use any contract with a compatible interface.
BATCH_PROXY_ADDRESS, const totalAmount: bigint
totalAmount],};
const const batchTransfer: { to: `0x${string}`; abi: readonly [{ readonly inputs: readonly [{ readonly name: "_tokenAddress"; readonly type: "address"; }, { readonly name: "_recipients"; readonly type: "address[]"; }, { readonly name: "_amounts"; readonly type: "uint256[]"; }, { readonly name: "_paymentReferences"; readonly type: "bytes[]"; }, { readonly name: "_feeAmounts"; readonly type: "uint256[]"; }, { readonly name: "_feeAddress"; readonly type: "address"; }]; readonly name: "batchERC20PaymentsWithReference"; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; readonly type: "function"; }]; functionName: string; args: (`0x${string}` | ... 1 more ... | bigint[])[];}
batchTransfer = { to: `0x${string}`
to: const BATCH_PROXY_ADDRESS: `0x${string}`
Batch proxy contract for multi-recipient transfers.
This example uses Request Finance's ERC20BatchPayments contract.
You can deploy your own or use any contract with a compatible interface.
BATCH_PROXY_ADDRESS, abi: readonly [{ readonly inputs: readonly [{ readonly name: "_tokenAddress"; readonly type: "address"; }, { readonly name: "_recipients"; readonly type: "address[]"; }, { readonly name: "_amounts"; readonly type: "uint256[]"; }, { readonly name: "_paymentReferences"; readonly type: "bytes[]"; }, { readonly name: "_feeAmounts"; readonly type: "uint256[]"; }, { readonly name: "_feeAddress"; readonly type: "address"; }]; readonly name: "batchERC20PaymentsWithReference"; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; readonly type: "function";}]
abi: const BATCH_PROXY_ABI: readonly [{ readonly inputs: readonly [{ readonly name: "_tokenAddress"; readonly type: "address"; }, { readonly name: "_recipients"; readonly type: "address[]"; }, { readonly name: "_amounts"; readonly type: "uint256[]"; }, { readonly name: "_paymentReferences"; readonly type: "bytes[]"; }, { readonly name: "_feeAmounts"; readonly type: "uint256[]"; }, { readonly name: "_feeAddress"; readonly type: "address"; }]; readonly name: "batchERC20PaymentsWithReference"; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; readonly type: "function";}]
BATCH_PROXY_ABI, functionName: string
functionName: "batchERC20PaymentsWithReference", args: (`0x${string}` | `0x${string}`[] | bigint[])[]
args: [ const zAssetAddress: Domain.ZAssetAddress
zAssetAddress, const payments: CreateBatchPaymentResult[]
payments.Array<CreateBatchPaymentResult>.map<Domain.ZAssetAddress>(callbackfn: (value: CreateBatchPaymentResult, index: number, array: CreateBatchPaymentResult[]) => Domain.ZAssetAddress, thisArg?: any): Domain.ZAssetAddress[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map((p: CreateBatchPaymentResult
p) => p: CreateBatchPaymentResult
p.CreateBatchPaymentResult.depositAddress: DepositAddress
Deposit address containing the zAsset address for transfers
depositAddress.DepositAddress.zAssetAddress: Domain.ZAssetAddress
zAssetAddress), const payments: CreateBatchPaymentResult[]
payments.Array<CreateBatchPaymentResult>.map<bigint>(callbackfn: (value: CreateBatchPaymentResult, index: number, array: CreateBatchPaymentResult[]) => bigint, thisArg?: any): bigint[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map((p: CreateBatchPaymentResult
p) => p: CreateBatchPaymentResult
p.CreateBatchPaymentResult.amount: bigint
Amount in the token's smallest unit
amount), const payments: CreateBatchPaymentResult[]
payments.Array<CreateBatchPaymentResult>.map<`0x${string}`>(callbackfn: (value: CreateBatchPaymentResult, index: number, array: CreateBatchPaymentResult[]) => `0x${string}`, thisArg?: any): `0x${string}`[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map(() => "0x00" as `0x${string}`), const payments: CreateBatchPaymentResult[]
payments.Array<CreateBatchPaymentResult>.map<bigint>(callbackfn: (value: CreateBatchPaymentResult, index: number, array: CreateBatchPaymentResult[]) => bigint, thisArg?: any): bigint[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map(() => 0n), const walletAddress: `0x${string}`
walletAddress, ],};
const const calls: Eip5792Call[]
calls = import EvmWalletClient
EvmWalletClient.const toBatchCalls: (inputs: ReadonlyArray<BatchCallInput>) => Eip5792Call[]
Convert SDK PreparedRequests and custom contract calls to EIP-5792 batch calls.
Use this to combine SDK-prepared transactions with your own contract calls
(e.g., a batch proxy) into a single batched transaction.
toBatchCalls([ const approveErc20: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
approveErc20, const wrap: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
wrap, const approveBatchProxy: { to: Domain.ZAssetAddress; abi: readonly [{ readonly inputs: readonly [{ readonly name: "spender"; readonly type: "address"; }, { readonly name: "amount"; readonly type: "uint256"; }]; readonly name: "approve"; readonly outputs: readonly [{ readonly name: ""; readonly type: "bool"; }]; readonly stateMutability: "nonpayable"; readonly type: "function"; }]; functionName: string; args: (bigint | `0x${string}`)[];}
approveBatchProxy, const batchTransfer: { to: `0x${string}`; abi: readonly [{ readonly inputs: readonly [{ readonly name: "_tokenAddress"; readonly type: "address"; }, { readonly name: "_recipients"; readonly type: "address[]"; }, { readonly name: "_amounts"; readonly type: "uint256[]"; }, { readonly name: "_paymentReferences"; readonly type: "bytes[]"; }, { readonly name: "_feeAmounts"; readonly type: "uint256[]"; }, { readonly name: "_feeAddress"; readonly type: "address"; }]; readonly name: "batchERC20PaymentsWithReference"; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; readonly type: "function"; }]; functionName: string; args: (`0x${string}` | ... 1 more ... | bigint[])[];}
batchTransfer,]);
const { const id: string
id } = await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.EvmWalletClient.sendCalls(calls: ReadonlyArray<EvmWalletClient.Eip5792Call>): Promise<EvmWalletClient.Eip5792SendCallsResult>
sendCalls(const calls: Eip5792Call[]
calls);
let let status: { atomic: boolean; capabilities?: { [key: string]: any; } | { [x: string]: any; } | undefined; chainId: number; id: string; receipts?: Viem.WalletCallReceipt<bigint, "success" | "reverted">[] | undefined; version: string; statusCode: number; status: "pending" | "success" | "failure" | undefined;}
status = await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.EvmWalletClient.getCallsStatus(id: string): Promise<EvmWalletClient.Eip5792CallsStatus>
getCallsStatus(const id: string
id);while (let status: { atomic: boolean; capabilities?: { [key: string]: any; } | { [x: string]: any; } | undefined; chainId: number; id: string; receipts?: Viem.WalletCallReceipt<bigint, "success" | "reverted">[] | undefined; version: string; statusCode: number; status: "pending" | "success" | "failure" | undefined;}
status.status: "success" | "pending" | "failure" | undefined
status === "pending") { await new var Promise: PromiseConstructornew <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
Creates a new Promise.
Promise((r: (value: unknown) => void
r) => function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+1 overload)
Schedules execution of a one-time callback after delay milliseconds.
The callback will likely not be invoked in precisely delay milliseconds.
Node.js makes no guarantees about the exact timing of when callbacks will fire,
nor of their ordering. The callback will be called as close as possible to the
time specified.
When delay is larger than 2147483647 or less than 1 or NaN, the delay
will be set to 1. Non-integer delays are truncated to an integer.
If callback is not a function, a TypeError will be thrown.
This method has a custom variant for promises that is available using
timersPromises.setTimeout().
setTimeout(r: (value: unknown) => void
r, 2000)); let status: { atomic: boolean; capabilities?: { [key: string]: any; } | { [x: string]: any; } | undefined; chainId: number; id: string; receipts?: Viem.WalletCallReceipt<bigint, "success" | "reverted">[] | undefined; version: string; statusCode: number; status: "pending" | "success" | "failure" | undefined;}
status = await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.EvmWalletClient.getCallsStatus(id: string): Promise<EvmWalletClient.Eip5792CallsStatus>
getCallsStatus(const id: string
id);}
const const lastBlockNumber: bigint | undefined
lastBlockNumber = let status: { atomic: boolean; capabilities?: { [key: string]: any; } | { [x: string]: any; } | undefined; chainId: number; id: string; receipts?: Viem.WalletCallReceipt<bigint, "success" | "reverted">[] | undefined; version: string; statusCode: number; status: "pending" | "success" | "failure" | undefined;}
status.receipts?: Viem.WalletCallReceipt<bigint, "success" | "reverted">[] | undefined
receipts?.[0]?.blockNumber: bigint | undefined
blockNumber;Update the light client so proofs can be generated against the latest state:
const const ibcHandlerAddress: Domain.IbcCoreAddress
ibcHandlerAddress = import IbcCoreRegistry
IbcCoreRegistry.const getIbcCoreAddressOrThrow: (ucid: string & Brand<"UniversalChainId">) => Domain.IbcCoreAddress
getIbcCoreAddressOrThrow( const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID,);
const const updateRequest: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
updateRequest = await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.WalletClient.updateLoopbackClient: (args: WalletClient.UpdateLoopbackClient) => Promise<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}>
updateLoopbackClient({ WalletClient.UpdateLoopbackClient.clientId: number
clientId: const CLIENT_ID: 5
CLIENT_ID, WalletClient.UpdateLoopbackClient.height: bigint
height: const lastBlockNumber: bigint | undefined
lastBlockNumber!, WalletClient.UpdateLoopbackClient.ibcHandlerAddress: Domain.IbcCoreAddress
ibcHandlerAddress, WalletClient.UpdateLoopbackClient.universalChainId: string & Brand<"UniversalChainId">
universalChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID,});
const [const signedUpdate: { readonly _tag: "SignedEvm"; readonly value: Viem.SignTransactionReturnType;}
signedUpdate] = await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.WalletClient.sign: (request: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;} | readonly { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[]) => Promise<...>
sign(const updateRequest: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
updateRequest);const const updateResult: { readonly _tag: "SubmissionEvm"; readonly hash: Domain.TxHash;}
updateResult = await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.WalletClient.submit: (request: { readonly _tag: "SignedEvm"; readonly value: Viem.SignTransactionReturnType;}) => Promise<{ readonly _tag: "SubmissionEvm"; readonly hash: Domain.TxHash;}>
submit(const signedUpdate: { readonly _tag: "SignedEvm"; readonly value: Viem.SignTransactionReturnType;}
signedUpdate);await const publicClient: EvmPublicClient.EvmPublicClient
publicClient.EvmPublicClient.waitForTransactionReceipt: (hash: Domain.TxHash) => Promise<Viem.TransactionReceipt>
waitForTransactionReceipt(const updateResult: { readonly _tag: "SubmissionEvm"; readonly hash: Domain.TxHash;}
updateResult.hash: Domain.TxHash
hash);Generate a zero-knowledge proof for each payee:
const const paymentsWithProofs: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}[]
paymentsWithProofs = await var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise.PromiseConstructor.all<Promise<{ nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}>[]>(values: Promise<{ nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}>[]): Promise<{ nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}[]> (+1 overload)
Creates a Promise that is resolved with an array of results when all of the provided Promises
resolve, or rejected when any Promise is rejected.
all( const payments: CreateBatchPaymentResult[]
payments.Array<CreateBatchPaymentResult>.map<Promise<{ nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}>>(callbackfn: (value: CreateBatchPaymentResult, index: number, array: CreateBatchPaymentResult[]) => Promise<{ nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}>, thisArg?: any): Promise<...>[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map(async (payment: CreateBatchPaymentResult
payment) => { const const nullifier: Domain.Nullifier
nullifier = await import Payment
Payment.const getNullifier: (options: { paymentKey: Domain.PaymentKey; destinationChainId: Domain.UniversalChainId;}) => Promise<Domain.Nullifier>
getNullifier({ paymentKey: Domain.PaymentKey
paymentKey: payment: CreateBatchPaymentResult
payment.CreateBatchPaymentResult.paymentKey: Domain.PaymentKey
Secret key for this payment - required for redemption
paymentKey, destinationChainId: string & Brand<"UniversalChainId">
destinationChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID, });
const const proof: Proof
proof = await import Payment
Payment.const generateProof: (options: { destinationPublicClient: PublicClient; sourcePublicClient: PublicClient; publicClient: PublicClient; prover: Prover.Prover;} & Parameters<typeof generateProof>[0]) => Promise<Proof>
generateProof({ paymentKey: Domain.PaymentKey
paymentKey: payment: CreateBatchPaymentResult
payment.CreateBatchPaymentResult.paymentKey: Domain.PaymentKey
Secret key for this payment - required for redemption
paymentKey, depositAddress: DepositAddress
depositAddress: payment: CreateBatchPaymentResult
payment.CreateBatchPaymentResult.depositAddress: DepositAddress
Deposit address containing the zAsset address for transfers
depositAddress, nullifier: Domain.Nullifier
nullifier, beneficiary: Domain.Erc20Address
beneficiary: payment: CreateBatchPaymentResult
payment.CreateBatchPaymentResult.beneficiary: Domain.Erc20Address
The recipient's address
beneficiary, amount: bigint
amount: payment: CreateBatchPaymentResult
payment.CreateBatchPaymentResult.amount: bigint
Amount in the token's smallest unit
amount, clientIds: number[]
clientIds: [const CLIENT_ID: 5
CLIENT_ID], selectedClientId: number
selectedClientId: const CLIENT_ID: 5
CLIENT_ID, srcChainId: string & Brand<"UniversalChainId">
srcChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID, srcErc20Address: Domain.Erc20Address
srcErc20Address: const USDC_ADDRESS: Domain.Erc20Address
USDC_ADDRESS, dstErc20Address: Domain.Erc20Address
dstErc20Address: const USDC_ADDRESS: Domain.Erc20Address
USDC_ADDRESS, publicClient: PublicClient
publicClient, sourcePublicClient: PublicClient
sourcePublicClient: const publicClient: EvmPublicClient.EvmPublicClient
publicClient, destinationPublicClient: PublicClient
destinationPublicClient: const publicClient: EvmPublicClient.EvmPublicClient
publicClient, prover: Prover.Prover
prover, });
return { ...payment: CreateBatchPaymentResult
payment, nullifier: Domain.Nullifier
nullifier, proof: Proof
proof }; }),);Fetch attestations for compliance and auditability:
const const paymentsWithAttestations: { attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}[]
paymentsWithAttestations = await var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise.PromiseConstructor.all<Promise<{ attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}>[]>(values: Promise<{ attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}>[]): Promise<...> (+1 overload)
Creates a Promise that is resolved with an array of results when all of the provided Promises
resolve, or rejected when any Promise is rejected.
all( const paymentsWithProofs: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}[]
paymentsWithProofs.Array<{ nullifier: Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint; }>.map<Promise<{ attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}>>(callbackfn: (value: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}, index: number, array: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}[]) => Promise<...>, thisArg?: any): Promise<...>[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map(async (payment: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}
payment) => { const const attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}
attestation = await const attestor: Attestor.Attestor
attestor.Attestor.get: (payload: { unspendableAddress: Address; beneficiary: Address;}) => Promise<{ readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}>
get({ unspendableAddress: `0x${string}`
unspendableAddress: payment: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}
payment.depositAddress: DepositAddress
Deposit address containing the zAsset address for transfers
depositAddress.DepositAddress.zAssetAddress: Domain.ZAssetAddress
zAssetAddress, beneficiary: `0x${string}`
beneficiary: payment: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}
payment.beneficiary: Domain.Erc20Address
The recipient's address
beneficiary, });
return { ...payment: { nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}
payment, attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}
attestation }; }),);Finally, submit redemptions to deliver funds to each recipient:
const const redemptionRequests: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[]
redemptionRequests = await var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise.PromiseConstructor.all<Promise<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}>[]>(values: Promise<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}>[]): Promise<...> (+1 overload)
Creates a Promise that is resolved with an array of results when all of the provided Promises
resolve, or rejected when any Promise is rejected.
all( const paymentsWithAttestations: { attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}[]
paymentsWithAttestations.Array<{ attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Nullifier; ... 4 more ...; amount: bigint; }>.map<Promise<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}>>(callbackfn: (value: { attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; ... 5 more ...; amount: bigint;}, index: number, array: { attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; ... 5 more ...; amount: bigint;}[]) => Promise<...>, thisArg?: any): Promise<...>[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map((p: { attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}
p) => import Payment
Payment.const prepareRedemption: (options: { destinationWalletClient: WalletClient;} & Parameters<typeof prepareRedemption>[0]) => Promise<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}>
prepareRedemption({ proof: Proof
proof: p: { attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}
p.proof: Proof
proof, attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}
attestation: p: { attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`; }; nullifier: Domain.Nullifier; proof: Proof; paymentKey: Domain.PaymentKey; depositAddress: DepositAddress; beneficiary: Domain.Erc20Address; amount: bigint;}
p.attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}
attestation, dstErc20Address: Domain.Erc20Address
dstErc20Address: const USDC_ADDRESS: Domain.Erc20Address
USDC_ADDRESS, destinationWalletClient: WalletClient
destinationWalletClient: const walletClient: EvmWalletClient.EvmWalletClient
walletClient, universalChainId: string & Brand<"UniversalChainId">
universalChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID, }), ),);
const const redemptionCalls: Eip5792Call[]
redemptionCalls = import EvmWalletClient
EvmWalletClient.const toBatchCalls: (inputs: ReadonlyArray<BatchCallInput>) => Eip5792Call[]
Convert SDK PreparedRequests and custom contract calls to EIP-5792 batch calls.
Use this to combine SDK-prepared transactions with your own contract calls
(e.g., a batch proxy) into a single batched transaction.
toBatchCalls(const redemptionRequests: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[]
redemptionRequests.Array<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: UniversalChainId; readonly contractAddress: Erc20Address; readonly abi: Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>; }>.flat<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[], 1>(this: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[], depth?: 1 | undefined): { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[]
Returns a new array with all sub-array elements concatenated into it recursively up to the
specified depth.
flat());await const walletClient: EvmWalletClient.EvmWalletClient
walletClient.EvmWalletClient.sendCalls(calls: ReadonlyArray<EvmWalletClient.Eip5792Call>): Promise<EvmWalletClient.Eip5792SendCallsResult>
sendCalls(const redemptionCalls: Eip5792Call[]
redemptionCalls);Privacy Guarantees
Section titled “Privacy Guarantees”With batch payroll:
- Recipients cannot see other recipients’ payment amounts
- Recipients cannot see the sender’s total balance or other funds
- Sender maintains full visibility of all payments for accounting
- Attestor records each (depositAddress, beneficiary) pair for compliance