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-sdk';
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
| Field | Type | Required | Description |
|---|---|---|---|
chainId | number | Yes | Numeric chain ID (use CHAIN_IDS.* constants) |
rpcUrl | string | Yes | RPC endpoint URL for the chain |
privateKey | `0x${string}` | Yes | 0x-prefixed private key for the signer |
options | Partial<OakContractsClientOptions> | No | Client-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-sdk';
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)
| Field | Type | Required | Description |
|---|---|---|---|
chainId | number | Yes | Numeric chain ID (use CHAIN_IDS.* constants) |
rpcUrl | string | Yes | RPC endpoint URL for the chain |
options | Partial<OakContractsClientOptions> | No | Client-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-sdk';
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-sdk';
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 });
| Field | Type | Required | Description |
|---|---|---|---|
chain | number | Chain | Yes | Numeric chain ID or viem Chain object |
provider | PublicClient | Yes | Viem PublicClient for on-chain reads |
signer | WalletClient | Yes | Viem WalletClient with an attached account |
options | Partial<OakContractsClientOptions> | No | Client-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-sdk';
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 });
Privy wallet with full configuration
Privy embedded wallets expose an EIP-1193 provider. Pass that provider to viem's custom transport for both createPublicClient and createWalletClient, then pass chain, provider, and signer into createOakContractsClient—the same pattern as Browser wallet with full configuration above.
The snippet uses the useWallets hook from @privy-io/react-auth to pick a wallet; replace that with whatever wallet selection logic your app uses.
import {
createOakContractsClient,
createPublicClient,
createWalletClient,
custom,
getChainFromId,
CHAIN_IDS,
} from '@oaknetwork/contracts-sdk';
import { useWallets } from '@privy-io/react-auth';
const { wallets } = useWallets();
const wallet = wallets[0]; // or select by address / connector
const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA);
await wallet.switchChain(chain.id); // ensure the wallet is on this chain
const ethereumProvider = await wallet.getEthereumProvider();
const provider = createPublicClient({
chain,
transport: custom(ethereumProvider),
});
const signer = createWalletClient({
chain,
transport: custom(ethereumProvider),
account: wallet.address as `0x${string}`,
});
const oak = createOakContractsClient({ chain, provider, signer });
If Privy does not include your chain in its default networks, register it in the Privy provider. See Configuring EVM networks in the Privy documentation.
Signer resolution priority
When a write or simulate method runs, the signer is resolved in this order:
- Per-call
options.signer— highest priority - Per-entity
signerpassed to the entity factory (e.g.oak.globalParams(addr, { signer })) - Client-level
walletClientfromcreateOakContractsClient(simple or full config with a wallet) - Throws an
ErrorfromrequireSigner()(message begins withNo signer configured.) if none of the above is set
Client options
| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | 30000 | Timeout 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:
| Property | Type | Description |
|---|---|---|
config | PublicOakContractsClientConfig | Public chain configuration (no secrets) |
options | OakContractsClientOptions | Resolved client options |
publicClient | PublicClient | Viem PublicClient for custom reads |
walletClient | WalletClient | null | Viem 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:
| Field | Type | Description |
|---|---|---|
blockNumber | bigint | Block in which the transaction was mined |
gasUsed | bigint | Total gas consumed |
logs | readonly { topics, data }[] | Raw log entries from the transaction |