Skip to content

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.

The batch payroll pattern extends the single payment flow to handle multiple payees efficiently:

  1. One wrap transaction for the total amount
  2. One batch transfer to all deposit addresses
  3. 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.

AspectSingle PaymentBatch Payroll
Payment keys1N (one per payee)
Wrap transactions11
Transfer transactions11 (via batch proxy)
Redemption transactions1N (or batched)

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"' &#x26;&#x26; 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.

@sincev0.1.27

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"' &#x26;&#x26; 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.

@sincev0.1.27

env
.
string | undefined
PRIVATE_KEY
as `0x${string}`;
if (!
const privateKey: `0x${string}`
privateKey
) throw new
var Error: ErrorConstructor
new (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.PrivateKeyAccount
export privateKeyToAccount

@description Creates an Account from a private key.

@returnsA Private Key Account.

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.

@paramconfig - PublicClientConfig

@returnsA Public Client. PublicClient

@example

import { createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains'

const client = createPublicClient({ chain: mainnet, transport: http(), })

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

@description Creates a HTTP transport that connects to a JSON-RPC API.

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:

@paramconfig - WalletClientConfig

@returnsA Wallet Client. WalletClient

@example

// JSON-RPC Account import { createWalletClient, custom } from 'viem' import { mainnet } from 'viem/chains'

const client = createWalletClient({ chain: mainnet, transport: custom(window.ethereum), })

@example

// Local Account import { createWalletClient, custom } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { mainnet } from 'viem/chains'

const client = createWalletClient({ account: privateKeyToAccount('0x…') chain: mainnet, transport: http(), })

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.PrivateKeyAccount
export privateKeyToAccount

@description Creates an Account from a private key.

@returnsA Private Key Account.

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

@description Creates a HTTP transport that connects to a JSON-RPC API.

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
({});

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