Skip to main content

Client Configuration

createOakContractsClient supports several config and signer patterns. You can mix them: for example, a read-only client with a per-entity signer for one contract, or a keyed client with per-call overrides for specific transactions.

Pattern 1 — Simple (chainId + rpcUrl + privateKey)

Full read/write access using a raw private key. Suitable for backend services and scripts.

import { createOakContractsClient, CHAIN_IDS } from '@oaknetwork/contracts';

const oak = createOakContractsClient({
chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA,
rpcUrl: 'https://forno.celo-sepolia.celo-testnet.org',
privateKey: '0x...', // 0x-prefixed 32-byte hex string
});

const gp = oak.globalParams('0x...');
const admin = await gp.getProtocolAdminAddress(); // read
await gp.enlistPlatform(hash, adminAddr, fee, adapter); // write — uses client key
FieldTypeRequiredDescription
chainIdnumberYesNumeric chain ID (use CHAIN_IDS.* constants)
rpcUrlstringYesRPC endpoint URL for the chain
privateKey`0x${string}`Yes0x-prefixed private key for the signer
optionsPartial<OakContractsClientOptions>NoClient-level overrides

Pattern 2 — Read-only (chainId + rpcUrl, no privateKey)

No private key required. All read methods work normally; write and simulate methods throw immediately — no RPC call is made. The error is thrown by requireSigner(); the message starts with No signer configured. and explains how to pass a client key, full-config signer, or per-entity signer (for example oak.globalParams(address, { signer })).

import { createOakContractsClient, CHAIN_IDS } from '@oaknetwork/contracts';

const oak = createOakContractsClient({
chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA,
rpcUrl: 'https://forno.celo-sepolia.celo-testnet.org',
});

const gp = oak.globalParams('0x...');
const admin = await gp.getProtocolAdminAddress(); // reads work fine
await gp.transferOwnership('0x...'); // throws (no signer — see requireSigner message)
FieldTypeRequiredDescription
chainIdnumberYesNumeric chain ID (use CHAIN_IDS.* constants)
rpcUrlstringYesRPC endpoint URL for the chain
optionsPartial<OakContractsClientOptions>NoClient-level overrides

Pattern 3 — Per-entity signer override

Pass a signer when creating an entity. Every write and simulate call on that entity uses the provided signer — you do not pass it again on each call. Use this when the signer is resolved after the client is created (browser wallets, Privy, etc.).

import { createOakContractsClient, createWallet, CHAIN_IDS } from '@oaknetwork/contracts';

const RPC_URL = 'https://forno.celo-sepolia.celo-testnet.org';

const oak = createOakContractsClient({
chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA,
rpcUrl: RPC_URL,
});

// Resolve signer after wallet connect
const signer = createWallet(privateKey, RPC_URL, oak.config.chain);
// or: const signer = await getSigner(window.ethereum, oak.config.chain);

// All write/simulate calls on gp use this signer
const gp = oak.globalParams('0x...', { signer });
const admin = await gp.getProtocolAdminAddress(); // read
await gp.simulate.enlistPlatform(hash, addr, fee, adapter); // simulate — uses signer
await gp.enlistPlatform(hash, addr, fee, adapter); // write — uses signer

Pattern 4 — Per-call signer override

The entity has no fixed signer. Pass a different signer for a single write or simulate call as the last optional argument. Use this when different operations on the same contract need different signers (multi-sig flows, role switching).

const gp = oak.globalParams('0x...'); // no entity-level signer

// Read — no signer needed
const admin = await gp.getProtocolAdminAddress();

// Write/simulate — signer only for this call
await gp.simulate.enlistPlatform(hash, addr, fee, adapter, { signer });
await gp.enlistPlatform(hash, addr, fee, adapter, { signer });

// Different call, different signer
await gp.transferOwnership(newOwner, { signer: anotherWallet });

// No override and no client/entity signer → throws (same requireSigner error as above)
await gp.delistPlatform(hash);

Pattern 5 — Full (bring your own clients)

Pass pre-built viem PublicClient and WalletClient directly. Use this for custom transports, account abstraction, or when you already manage viem clients elsewhere.

import {
createOakContractsClient,
createPublicClient,
createWalletClient,
http,
getChainFromId,
CHAIN_IDS,
} from '@oaknetwork/contracts';

const RPC_URL = 'https://forno.celo-sepolia.celo-testnet.org';

const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA);
const provider = createPublicClient({ chain, transport: http(RPC_URL) });
const signer = createWalletClient({ account, chain, transport: http(RPC_URL) });

const oak = createOakContractsClient({ chain, provider, signer });
FieldTypeRequiredDescription
chainnumber | ChainYesNumeric chain ID or viem Chain object
providerPublicClientYesViem PublicClient for on-chain reads
signerWalletClientYesViem WalletClient with an attached account
optionsPartial<OakContractsClientOptions>NoClient-level overrides

Browser wallet with full configuration

If you prefer to construct the client with provider and signer up front (instead of Pattern 3 on a read-only or minimal client):

import {
createOakContractsClient,
createBrowserProvider,
getSigner,
getChainFromId,
CHAIN_IDS,
} from '@oaknetwork/contracts';

const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA);
const provider = createBrowserProvider(window.ethereum, chain);
const signer = await getSigner(window.ethereum, chain);

const oak = createOakContractsClient({ chain, provider, signer });

Signer resolution priority

When a write or simulate method runs, the signer is resolved in this order:

  1. Per-call options.signer — highest priority
  2. Per-entity signer passed to the entity factory (e.g. oak.globalParams(addr, { signer }))
  3. Client-level walletClient from createOakContractsClient (simple or full config with a wallet)
  4. Throws an Error from requireSigner() (message begins with No signer configured.) if none of the above is set

Client options

OptionTypeDefaultDescription
timeoutnumber30000Timeout in milliseconds for transport calls and waitForTransactionReceipt
const oak = createOakContractsClient({
chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA,
rpcUrl: 'https://forno.celo-sepolia.celo-testnet.org',
privateKey: '0x...',
options: {
timeout: 60000, // 60 seconds
},
});

Client properties

Once created, the client exposes these read-only properties:

PropertyTypeDescription
configPublicOakContractsClientConfigPublic chain configuration (no secrets)
optionsOakContractsClientOptionsResolved client options
publicClientPublicClientViem PublicClient for custom reads
walletClientWalletClient | nullViem WalletClient for custom writes (null for read-only clients)

Waiting for receipts

Write methods return a transaction hash (Hex). Use waitForReceipt() to poll until the transaction is mined.

const txHash = await gp.enlistPlatform(platformHash, admin, fee, adapter);

const receipt = await oak.waitForReceipt(txHash);

console.log('Block:', receipt.blockNumber);
console.log('Gas used:', receipt.gasUsed);
console.log('Logs:', receipt.logs.length);

The receipt includes:

FieldTypeDescription
blockNumberbigintBlock in which the transaction was mined
gasUsedbigintTotal gas consumed
logsreadonly { topics, data }[]Raw log entries from the transaction