@oaknetwork/contracts-sdk
v1.1.1
Published
TypeScript SDK for Oak Network smart contracts
Readme
Oak Contracts SDK
TypeScript SDK for interacting with Oak Network smart contracts. Provides a type-safe client with full read/write access to all Oak protocol contracts.
Full Documentation: oaknetwork.org/docs/contracts-sdk/overview
Prerequisites
You need deployed contract addresses to use this SDK.
The SDK interacts with Oak Network smart contracts that must already be deployed on-chain. To get your contract addresses and sandbox environment access, contact our team at [email protected].
Installation
pnpm add @oaknetwork/contracts-sdknpm install @oaknetwork/contracts-sdkyarn add @oaknetwork/contracts-sdkRequirements: Node.js 18+, TypeScript 5.x recommended.
Supported Chain IDs
import { CHAIN_IDS } from "@oaknetwork/contracts-sdk";
CHAIN_IDS.ETHEREUM_MAINNET; // 1
CHAIN_IDS.CELO_MAINNET; // 42220
CHAIN_IDS.ETHEREUM_TESTNET_SEPOLIA; // 11155111
CHAIN_IDS.ETHEREUM_TESTNET_GOERLI; // 5
CHAIN_IDS.CELO_TESTNET_SEPOLIA; // 11142220See the full installation documentation here: oaknetwork.org/docs/contracts-sdk/installation
Quick Start
Create a client
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...",
});See the full Quickstart guide for a step-by-step walkthrough.
Client Configuration
Four config/signer patterns are supported. Mix and match as needed.
Pattern 1 — Simple (chainId + rpcUrl + privateKey)
Full read/write access using a raw private key. Suitable for backend services and scripts.
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 keyPattern 2 — Read-only (chainId + rpcUrl, no privateKey)
No private key required. All read methods work normally; write methods throw "No signer configured" immediately — no RPC call is made.
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 configured"Pattern 3 — Per-entity signer override
Supply a signer when creating an entity. Every write/simulate call on that entity uses the provided signer — no need to pass it again per call. Designed for browser wallets (MetaMask, Privy, etc.) where the signer is resolved after the client is created.
import {
createOakContractsClient,
createWallet,
CHAIN_IDS,
} from "@oaknetwork/contracts-sdk";
const oak = createOakContractsClient({
chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA,
rpcUrl: "https://forno.celo-sepolia.celo-testnet.org",
});
// Resolve signer after wallet connect
const signer = createWallet(privateKey, rpcUrl, oak.config.chain);
// or: const signer = await getSigner(window.ethereum, oak.config.chain);
// All write/simulate calls on gp automatically use 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 signerPattern 4 — Per-call signer override
Supply a different signer for a single write or simulate call. The entity itself has no fixed signer; the override is passed as the last optional argument. Useful when different operations on the same contract require different signers (e.g. 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 — inject signer only for this one 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 → throws "No signer configured"
await gp.delistPlatform(hash); // throws if no client/entity signer setPattern 5 — Full (bring your own clients)
Pass pre-built viem PublicClient and WalletClient directly. Useful for advanced configurations (custom transports, account abstraction, etc.).
import {
createOakContractsClient,
createPublicClient,
createWalletClient,
http,
getChainFromId,
CHAIN_IDS,
} from "@oaknetwork/contracts-sdk";
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 });Signer resolution priority
When a write or simulate method is called, the signer is resolved in this order:
- Per-call
options.signer— highest priority - Per-entity
signerpassed to the entity factory method - Client-level
walletClientfromcreateOakContractsClient - Throws
"No signer configured"if none of the above is set
For a detailed step-by-step guide, please refer to the complete Client Configuration documentation.
Contract Entities
GlobalParams
Protocol-wide configuration registry. Manages platform listings, fee settings, token currencies, line item types, and a general-purpose key-value registry.
const gp = oak.globalParams("0x...");
// Reads
const admin = await gp.getProtocolAdminAddress();
const fee = await gp.getProtocolFeePercent(); // bigint bps (e.g. 100 = 1%)
const count = await gp.getNumberOfListedPlatforms();
const isListed = await gp.checkIfPlatformIsListed(platformHash);
const platAdmin = await gp.getPlatformAdminAddress(platformHash);
const platFee = await gp.getPlatformFeePercent(platformHash);
const delay = await gp.getPlatformClaimDelay(platformHash);
const adapter = await gp.getPlatformAdapter(platformHash);
const tokens = await gp.getTokensForCurrency(currency); // Address[]
const lineItem = await gp.getPlatformLineItemType(platformHash, typeId);
const value = await gp.getFromRegistry(key);
// Writes
await gp.enlistPlatform(platformHash, adminAddress, feePercent, adapterAddress);
await gp.delistPlatform(platformHash);
await gp.updatePlatformAdminAddress(platformHash, newAdmin);
await gp.updatePlatformClaimDelay(platformHash, delaySeconds);
await gp.updateProtocolAdminAddress(newAdmin);
await gp.updateProtocolFeePercent(newFeePercent);
await gp.setPlatformAdapter(platformHash, adapterAddress);
await gp.setPlatformLineItemType(
platformHash,
typeId,
label,
countsTowardGoal,
applyProtocolFee,
canRefund,
instantTransfer,
);
await gp.removePlatformLineItemType(platformHash, typeId);
await gp.addTokenToCurrency(currency, tokenAddress);
await gp.removeTokenFromCurrency(currency, tokenAddress);
await gp.addPlatformData(platformHash, platformDataKey);
await gp.removePlatformData(platformHash, platformDataKey);
await gp.addToRegistry(key, value);
await gp.transferOwnership(newOwner);For complete details on the Global Params contract entity, please visit the following link: Global Params.
CampaignInfoFactory
Deploys new CampaignInfo contracts. Each campaign gets its own on-chain CampaignInfo instance with its own address, NFT collection, and configuration.
import {
createOakContractsClient,
keccak256,
toHex,
getCurrentTimestamp,
addDays,
CHAIN_IDS,
} from "@oaknetwork/contracts-sdk";
const factory = oak.campaignInfoFactory("0x...");
const PLATFORM_HASH = keccak256(toHex("my-platform"));
const CURRENCY = toHex("USD", { size: 32 });
const identifierHash = keccak256(toHex("my-campaign-slug"));
const now = getCurrentTimestamp();
// Reads
const infoAddress = await factory.identifierToCampaignInfo(identifierHash);
const isValid = await factory.isValidCampaignInfo(infoAddress);
// Writes
const txHash = await factory.createCampaign({
creator: "0x...",
identifierHash,
selectedPlatformHash: [PLATFORM_HASH],
campaignData: {
launchTime: now + 3_600n, // 1 hour from now
deadline: addDays(now, 30), // 30 days from now
goalAmount: 1_000_000n,
currency: CURRENCY,
},
nftName: "My Campaign NFT",
nftSymbol: "MCN",
nftImageURI: "https://example.com/nft.png",
contractURI: "https://example.com/contract.json",
});
const receipt = await oak.waitForReceipt(txHash);
const campaignAddress = await factory.identifierToCampaignInfo(identifierHash);For complete details on the Campaign Info Factory contract entity, please visit the following link: Campaign Info Factory.
CampaignInfo
Per-campaign configuration and state. Each campaign deployed via the CampaignInfoFactory gets its own CampaignInfo contract that tracks funding progress, accepted tokens, platform settings, and NFT pledge records.
const ci = oak.campaignInfo("0x...");
// Reads
const launchTime = await ci.getLaunchTime();
const deadline = await ci.getDeadline();
const goalAmount = await ci.getGoalAmount();
const currency = await ci.getCampaignCurrency();
const totalRaised = await ci.getTotalRaisedAmount();
const available = await ci.getTotalAvailableRaisedAmount();
const isLocked = await ci.isLocked();
const isCancelled = await ci.cancelled();
const config = await ci.getCampaignConfig();
const tokens = await ci.getAcceptedTokens();
// Writes
await ci.updateDeadline(newDeadline);
await ci.updateGoalAmount(newGoal);
await ci.pauseCampaign(message);
await ci.unpauseCampaign(message);
await ci.cancelCampaign(message);For complete details on the Campaign Info contract entity, please visit the following link: Campaign Info.
TreasuryFactory
Deploys treasury contracts for a given CampaignInfo. Manages treasury implementations that platforms can register, approve, and deploy.
const tf = oak.treasuryFactory("0x...");
// Deploy
const txHash = await tf.deploy(platformHash, infoAddress, implementationId);
// Implementation management
await tf.registerTreasuryImplementation(
platformHash,
implementationId,
implAddress,
);
await tf.approveTreasuryImplementation(platformHash, implementationId);
await tf.disapproveTreasuryImplementation(implAddress);
await tf.removeTreasuryImplementation(platformHash, implementationId);For complete details on the Treasury Factory contract entity, please visit the following link: Treasury Factory.
PaymentTreasury
Handles fiat-style payments via a payment gateway. Manages payment creation, confirmation, refunds, fee disbursement, and fund withdrawal for campaigns.
Two treasury variants, one SDK method. The
paymentTreasury()method works with both on-chain implementations:| Variant | Description | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PaymentTreasury | Standard payment treasury with no time restrictions. Payments can be created, confirmed, and refunded at any time while the treasury is active. | | TimeConstrainedPaymentTreasury | Time-constrained variant that enforces launch-time and deadline windows on-chain. Payments can only be created within the campaign window (launch → deadline + buffer). Refunds, withdrawals, and fee disbursements are only available after launch. |
Both contracts share the same ABI and the same SDK interface. Time enforcement is handled entirely on-chain — simply pass the deployed contract address regardless of which variant was deployed:
// Works for both PaymentTreasury and TimeConstrainedPaymentTreasury
const pt = oak.paymentTreasury("0x...");
// Reads
const raised = await pt.getRaisedAmount();
const refunded = await pt.getRefundedAmount();
const payment = await pt.getPaymentData(paymentId);
// Writes
const txHash = await pt.createPayment(
paymentId,
buyerId,
itemId,
paymentToken,
amount,
expiration,
lineItems,
externalFees,
);
await pt.confirmPayment(paymentId, buyerAddress);
await pt.claimRefund(paymentId, refundAddress);
await pt.claimRefundSelf(paymentId);
await pt.disburseFees();
await pt.withdraw();
await pt.pauseTreasury(message);
await pt.unpauseTreasury(message);
await pt.cancelTreasury(message);Note: When using a
TimeConstrainedPaymentTreasury, calls made outside the allowed time window will revert on-chain. For example,createPayment()will revert if called before launch or after the deadline + buffer period.
For complete details on the Payment Treasury contract entity, please visit the following link: Payment Treasury.
AllOrNothing Treasury
Crowdfunding treasury where funds are only released if the campaign goal is met. If the goal is not reached, backers can claim full refunds. Includes ERC-721 pledge NFTs.
const aon = oak.allOrNothingTreasury("0x...");
// Reads
const raised = await aon.getRaisedAmount();
const reward = await aon.getReward(rewardName);
// Writes
await aon.addRewards(rewardNames, rewards);
await aon.pledgeForAReward(backer, pledgeToken, shippingFee, rewardNames);
await aon.pledgeWithoutAReward(backer, pledgeToken, pledgeAmount);
await aon.claimRefund(tokenId);
await aon.disburseFees();
await aon.withdraw();
await aon.pauseTreasury(message);
await aon.unpauseTreasury(message);
await aon.cancelTreasury(message);
// ERC-721
const owner = await aon.ownerOf(tokenId);
const uri = await aon.tokenURI(tokenId);
await aon.safeTransferFrom(from, to, tokenId);For complete details on the AllOrNothing Treasury contract entity, please visit the following link: AllOrNothing Treasury.
KeepWhatsRaised Treasury
Crowdfunding treasury where the creator keeps all funds raised regardless of whether the goal is met. Includes configurable fee structures, withdrawal delays, and ERC-721 pledge NFTs.
const kwr = oak.keepWhatsRaisedTreasury("0x...");
// Reads
const raised = await kwr.getRaisedAmount();
const available = await kwr.getAvailableRaisedAmount();
const reward = await kwr.getReward(rewardName);
// Writes
await kwr.configureTreasury(config, campaignData, feeKeys, feeValues);
await kwr.addRewards(rewardNames, rewards);
await kwr.pledgeForAReward(pledgeId, backer, token, tip, rewardNames);
await kwr.pledgeWithoutAReward(pledgeId, backer, token, amount, tip);
await kwr.approveWithdrawal();
await kwr.claimFund();
await kwr.claimTip();
await kwr.claimRefund(tokenId);
await kwr.disburseFees();
await kwr.withdraw(token, amount);
await kwr.pauseTreasury(message);
await kwr.unpauseTreasury(message);
await kwr.cancelTreasury(message);For complete details on the KeepWhatsRaised Treasury contract entity, please visit the following link: KeepWhatsRaised Treasury.
ItemRegistry
Manages items available for purchase in campaigns. Items represent physical goods with dimensions, weight, and category metadata.
const ir = oak.itemRegistry("0x...");
// Read
const item = await ir.getItem(ownerAddress, itemId);
// Writes
await ir.addItem(itemId, item);
await ir.addItemsBatch(itemIds, items);For complete details on the Item Registry contract entity, please visit the following link: Item Registry.
Metrics
Pre-built aggregation functions that combine multiple on-chain reads into meaningful reports. Import from @oaknetwork/contracts-sdk/metrics.
Platform Stats
Protocol-level statistics from GlobalParams:
import { getPlatformStats } from "@oaknetwork/contracts-sdk/metrics";
const stats = await getPlatformStats({
globalParamsAddress: "0x...",
publicClient: oak.publicClient,
});
console.log(`${stats.platformCount} platforms enlisted`);
console.log(`Protocol fee: ${stats.protocolFeePercent} bps`);Campaign Summary
Financial aggregation from a deployed CampaignInfo contract:
import { getCampaignSummary } from "@oaknetwork/contracts-sdk/metrics";
const summary = await getCampaignSummary({
campaignInfoAddress: "0x...",
publicClient: oak.publicClient,
});
console.log(`Total raised: ${summary.totalRaised}`);
console.log(`Goal: ${summary.goalAmount}`);
console.log(`Goal reached: ${summary.goalReached}`);
console.log(`Refunded: ${summary.totalRefunded}`);Treasury Report
Per-treasury financial report for any treasury type:
import { getTreasuryReport } from "@oaknetwork/contracts-sdk/metrics";
const report = await getTreasuryReport({
treasuryAddress: "0x...",
treasuryType: "all-or-nothing", // or "keep-whats-raised" | "payment-treasury"
publicClient: oak.publicClient,
});
console.log(`Raised: ${report.raisedAmount}`);
console.log(`Refunded: ${report.refundedAmount}`);
console.log(`Fee: ${report.platformFeePercent} bps`);
console.log(`Cancelled: ${report.cancelled}`);For complete metrics documentation, see: Metrics.
Events
Every contract entity exposes an events property with three capabilities:
- Fetch historical logs — query past event logs from the blockchain
- Decode raw logs — parse raw transaction receipt logs into typed event objects
- Watch live events — subscribe to real-time event notifications
Fetching historical logs
Each event has a get*Logs() method that returns all matching logs from the entire chain history. You can optionally pass { fromBlock, toBlock } to narrow the search range.
const gp = oak.globalParams("0x...");
// All PlatformEnlisted events ever emitted by this contract
const logs = await gp.events.getPlatformEnlistedLogs();
for (const log of logs) {
console.log(log.eventName); // "PlatformEnlisted"
console.log(log.args); // { platformHash: "0x...", adminAddress: "0x...", ... }
}
// Filter by block range
const recentLogs = await gp.events.getPlatformEnlistedLogs({
fromBlock: 1_000_000n,
toBlock: 2_000_000n,
});Decoding raw logs
Use decodeLog() to decode a raw log from a transaction receipt. This is useful when you have a receipt and want to decode its logs without knowing which event they belong to.
const receipt = await oak.waitForReceipt(txHash);
for (const log of receipt.logs) {
try {
const decoded = gp.events.decodeLog({
topics: log.topics,
data: log.data,
});
console.log(decoded.eventName, decoded.args);
} catch {
// Log doesn't match any event in this contract's ABI
}
}Watching live events
Each event has a watch*() method that subscribes to real-time event notifications. The method returns an unwatch function to stop listening.
const gp = oak.globalParams("0x...");
// Start watching for new PlatformEnlisted events
const unwatch = gp.events.watchPlatformEnlisted((logs) => {
for (const log of logs) {
console.log("New platform enlisted:", log.args);
}
});
// Later — stop watching
unwatch();Available events per contract
GlobalParams
const gp = oak.globalParams("0x...");
// Fetch historical logs
await gp.events.getPlatformEnlistedLogs(options?);
await gp.events.getPlatformDelistedLogs(options?);
await gp.events.getPlatformAdminAddressUpdatedLogs(options?);
await gp.events.getPlatformDataAddedLogs(options?);
await gp.events.getPlatformDataRemovedLogs(options?);
await gp.events.getPlatformAdapterSetLogs(options?);
await gp.events.getPlatformClaimDelayUpdatedLogs(options?);
await gp.events.getProtocolAdminAddressUpdatedLogs(options?);
await gp.events.getProtocolFeePercentUpdatedLogs(options?);
await gp.events.getTokenAddedToCurrencyLogs(options?);
await gp.events.getTokenRemovedFromCurrencyLogs(options?);
await gp.events.getOwnershipTransferredLogs(options?);
await gp.events.getPausedLogs(options?);
await gp.events.getUnpausedLogs(options?);
// Decode a raw log
gp.events.decodeLog({ topics, data });
// Watch live events
const unwatch = gp.events.watchPlatformEnlisted(handler);
const unwatch = gp.events.watchPlatformDelisted(handler);
const unwatch = gp.events.watchTokenAddedToCurrency(handler);
const unwatch = gp.events.watchTokenRemovedFromCurrency(handler);CampaignInfoFactory
const factory = oak.campaignInfoFactory("0x...");
await factory.events.getCampaignCreatedLogs(options?);
await factory.events.getCampaignInitializedLogs(options?);
await factory.events.getOwnershipTransferredLogs(options?);
factory.events.decodeLog({ topics, data });
const unwatch = factory.events.watchCampaignCreated(handler);TreasuryFactory
const tf = oak.treasuryFactory("0x...");
await tf.events.getTreasuryDeployedLogs(options?);
await tf.events.getImplementationRegisteredLogs(options?);
await tf.events.getImplementationRemovedLogs(options?);
await tf.events.getImplementationApprovalLogs(options?);
tf.events.decodeLog({ topics, data });
const unwatch = tf.events.watchTreasuryDeployed(handler);
const unwatch = tf.events.watchImplementationRegistered(handler);CampaignInfo
const ci = oak.campaignInfo("0x...");
await ci.events.getDeadlineUpdatedLogs(options?);
await ci.events.getGoalAmountUpdatedLogs(options?);
await ci.events.getLaunchTimeUpdatedLogs(options?);
await ci.events.getPlatformInfoUpdatedLogs(options?);
await ci.events.getSelectedPlatformUpdatedLogs(options?);
await ci.events.getOwnershipTransferredLogs(options?);
await ci.events.getPausedLogs(options?);
await ci.events.getUnpausedLogs(options?);
ci.events.decodeLog({ topics, data });
const unwatch = ci.events.watchDeadlineUpdated(handler);
const unwatch = ci.events.watchPlatformInfoUpdated(handler);
const unwatch = ci.events.watchSelectedPlatformUpdated(handler);PaymentTreasury
const pt = oak.paymentTreasury("0x...");
await pt.events.getPaymentCreatedLogs(options?);
await pt.events.getPaymentCancelledLogs(options?);
await pt.events.getPaymentConfirmedLogs(options?);
await pt.events.getPaymentBatchConfirmedLogs(options?);
await pt.events.getPaymentBatchCreatedLogs(options?);
await pt.events.getFeesDisbursedLogs(options?);
await pt.events.getWithdrawalWithFeeSuccessfulLogs(options?);
await pt.events.getRefundClaimedLogs(options?);
await pt.events.getNonGoalLineItemsClaimedLogs(options?);
await pt.events.getExpiredFundsClaimedLogs(options?);
pt.events.decodeLog({ topics, data });
const unwatch = pt.events.watchPaymentCreated(handler);
const unwatch = pt.events.watchPaymentConfirmed(handler);
const unwatch = pt.events.watchPaymentCancelled(handler);
const unwatch = pt.events.watchRefundClaimed(handler);
const unwatch = pt.events.watchFeesDisbursed(handler);AllOrNothing Treasury
const aon = oak.allOrNothingTreasury("0x...");
await aon.events.getReceiptLogs(options?);
await aon.events.getRefundClaimedLogs(options?);
await aon.events.getWithdrawalSuccessfulLogs(options?);
await aon.events.getFeesDisbursedLogs(options?);
await aon.events.getRewardsAddedLogs(options?);
await aon.events.getRewardRemovedLogs(options?);
await aon.events.getPausedLogs(options?);
await aon.events.getUnpausedLogs(options?);
await aon.events.getTransferLogs(options?);
await aon.events.getSuccessConditionNotFulfilledLogs(options?);
aon.events.decodeLog({ topics, data });
const unwatch = aon.events.watchReceipt(handler);
const unwatch = aon.events.watchRefundClaimed(handler);
const unwatch = aon.events.watchWithdrawalSuccessful(handler);
const unwatch = aon.events.watchFeesDisbursed(handler);KeepWhatsRaised Treasury
const kwr = oak.keepWhatsRaisedTreasury("0x...");
await kwr.events.getReceiptLogs(options?);
await kwr.events.getRefundClaimedLogs(options?);
await kwr.events.getWithdrawalWithFeeSuccessfulLogs(options?);
await kwr.events.getWithdrawalApprovedLogs(options?);
await kwr.events.getFeesDisbursedLogs(options?);
await kwr.events.getTreasuryConfiguredLogs(options?);
await kwr.events.getRewardsAddedLogs(options?);
await kwr.events.getRewardRemovedLogs(options?);
await kwr.events.getTipClaimedLogs(options?);
await kwr.events.getFundClaimedLogs(options?);
await kwr.events.getDeadlineUpdatedLogs(options?);
await kwr.events.getGoalAmountUpdatedLogs(options?);
await kwr.events.getPaymentGatewayFeeSetLogs(options?);
await kwr.events.getPausedLogs(options?);
await kwr.events.getUnpausedLogs(options?);
await kwr.events.getTransferLogs(options?);
kwr.events.decodeLog({ topics, data });
const unwatch = kwr.events.watchReceipt(handler);
const unwatch = kwr.events.watchRefundClaimed(handler);
const unwatch = kwr.events.watchWithdrawalWithFeeSuccessful(handler);
const unwatch = kwr.events.watchFeesDisbursed(handler);ItemRegistry
const ir = oak.itemRegistry("0x...");
await ir.events.getItemAddedLogs(options?);
ir.events.decodeLog({ topics, data });
const unwatch = ir.events.watchItemAdded(handler);Types
All event methods use shared types from @oaknetwork/contracts-sdk:
import type {
DecodedEventLog,
EventFilterOptions,
EventWatchHandler,
RawLog,
} from "@oaknetwork/contracts-sdk";
// EventFilterOptions — optional block range for get*Logs
interface EventFilterOptions {
fromBlock?: bigint; // defaults to 0n (genesis) if omitted
toBlock?: bigint; // defaults to latest block if omitted
}
// DecodedEventLog — returned by get*Logs and decodeLog
interface DecodedEventLog {
eventName: string;
args: Record<string, unknown>;
}
// RawLog — input to decodeLog
interface RawLog {
topics: readonly `0x${string}`[];
data: `0x${string}`;
}
// EventWatchHandler — callback for watch* methods
type EventWatchHandler = (logs: readonly DecodedEventLog[]) => void;For complete details on contract events, please visit the following link: Events.
Error Handling
Contract calls can revert with on-chain errors. The SDK decodes raw revert data into typed error classes with decoded arguments and human-readable recovery hints.
Decoding revert errors:
import { parseContractError, getRevertData } from "@oaknetwork/contracts-sdk";
function handleError(err) {
// If the error is already a typed SDK error (thrown by simulate methods)
if (typeof err?.recoveryHint === "string") {
console.error("Reverted:", err.name);
console.error("Args:", err.args);
console.error("Hint:", err.recoveryHint);
return;
}
// Otherwise extract raw revert hex from the viem error chain and decode it
const revertData = getRevertData(err);
const parsed = parseContractError(revertData ?? "");
if (parsed) {
console.error("Reverted:", parsed.name);
console.error("Args:", parsed.args);
if (parsed.recoveryHint) console.error("Hint:", parsed.recoveryHint);
return;
}
console.error("Unknown error:", err.message);
}
try {
const txHash = await factory.createCampaign({ ... });
} catch (err) {
handleError(err);
}See the full error handling guidelines here: Error handling
Utility Functions
The SDK exports pure utility functions and constants that have no client dependency. Import them from @oaknetwork/contracts-sdk or @oaknetwork/contracts-sdk/utils.
import {
keccak256,
id,
toHex,
stringToHex,
parseEther,
formatEther,
parseUnits,
isAddress,
getAddress,
getCurrentTimestamp,
addDays,
getChainFromId,
multicall,
createJsonRpcProvider,
createWallet,
createBrowserProvider,
getSigner,
CHAIN_IDS,
BPS_DENOMINATOR,
BYTES32_ZERO,
DATA_REGISTRY_KEYS,
scopedToPlatform,
} from "@oaknetwork/contracts-sdk";
// Hash a string to bytes32
const platformHash = keccak256(toHex("my-platform"));
// Encode string to fixed bytes32
const currency = toHex("USD", { size: 32 });
// Timestamp helpers
const now = getCurrentTimestamp(); // bigint seconds
const deadline = addDays(now, 30); // 30 days from now
// Fee calculations (fees are in basis points, 10_000 = 100%)
const feeAmount = (raisedAmount * platformFee) / BPS_DENOMINATOR;
// Browser wallet (frontend)
const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA);
const provider = createBrowserProvider(window.ethereum, chain);
const signer = await getSigner(window.ethereum, chain);For complete guidelines on utility functions, please refer to the following link: Utility Functions.
Exported Entry Points
| Entry point | Contents |
| ------------------------------------- | ------------------------------------------------------------------------------ |
| @oaknetwork/contracts-sdk | Everything — client, types, utils, errors |
| @oaknetwork/contracts-sdk/utils | Utility functions only (no client) |
| @oaknetwork/contracts-sdk/contracts | Contract entity factories only |
| @oaknetwork/contracts-sdk/client | createOakContractsClient only |
| @oaknetwork/contracts-sdk/errors | Error classes and parseContractError only |
| @oaknetwork/contracts-sdk/metrics | Platform, campaign, and treasury reporting helpers (not re-exported from root) |
Multicall
Batch multiple entity read calls into a single RPC round-trip via the on-chain Multicall3 contract. Pass an array of lazy closures — the same entity read methods you'd normally await individually.
Standalone utility
import { multicall } from "@oaknetwork/contracts-sdk";
const gp = oak.globalParams("0x...");
const [platformCount, feePercent, admin] = await multicall([
() => gp.getNumberOfListedPlatforms(),
() => gp.getProtocolFeePercent(),
() => gp.getProtocolAdminAddress(),
]);Client convenience method
const gp = oak.globalParams("0x...");
const [count, fee] = await oak.multicall([
() => gp.getNumberOfListedPlatforms(),
() => gp.getProtocolFeePercent(),
]);Cross-contract batching
Reads from different entities are batched into one RPC call automatically:
const gp = oak.globalParams("0x...");
const ci = oak.campaignInfo("0x...");
const aon = oak.allOrNothingTreasury("0x...");
const [platformCount, goalAmount, raisedAmount] = await oak.multicall([
() => gp.getNumberOfListedPlatforms(),
() => ci.getGoalAmount(),
() => aon.getRaisedAmount(),
]);Under the hood, the SDK enables viem's
batch.multicalltransport option. AllreadContractcalls dispatched within the same tick are automatically aggregated into a single Multicall3 on-chain call — no raw ABI descriptors needed.
Local Development & Testing
Install dependencies
pnpm installBuild
pnpm buildDo not use npm or yarn. The repository enforces pnpm >= 10.0.0.
Running tests
pnpm test:unit # Unit tests
pnpm test:integration # Integration tests (requires credentials)
pnpm test:all # All tests with coverage
pnpm test:watch # Watch modeChangesets Workflow
We use Changesets to manage versions and changelogs:
- After making changes, run
pnpm changeset - Select impact (Major / Minor / Patch) for affected packages
- Commit the generated file in
.changeset/ - CI automatically calculates versions, generates changelogs, and creates a release PR
Development Guidelines
See CLAUDE.md for coding standards including architecture principles, security rules, testing requirements, and anti-patterns.
Code review checklist
pnpm buildsucceedspnpm testpasses with >90% coveragepnpm linthas no errors- Changeset created with
pnpm changeset - Documentation updated if needed
Documentation
- Full docs — oaknetwork.org/docs/contracts-sdk/overview
- Quickstart — oaknetwork.org/docs/contracts-sdk/quickstart
- Monorepo README — README.md
- Changelog — CHANGELOG.md
License
Security
Links
Questions? Open an issue or contact [email protected]
