Skip to content

API Reference

Practical guide to interacting with the FairWins contracts from JavaScript (ethers.js v6). Every interaction is an on-chain transaction or view call — there is no HTTP API and no backend.

For exact signatures see Contract Interfaces; for addresses see the Smart Contracts guide.

Setup

ABIs are produced by npx hardhat compile under artifacts/contracts/ (the frontend keeps trimmed copies in frontend/src/abis/).

import { ethers } from "ethers";

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

const registry = new ethers.Contract(WAGER_REGISTRY_ADDRESS, WagerRegistryABI, signer);
const membership = new ethers.Contract(MEMBERSHIP_MANAGER_ADDRESS, MembershipManagerABI, signer);
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, signer);

USDC has 6 decimals — use ethers.parseUnits("10", 6) for a 10 USDC stake.

Membership

Creating or accepting wagers requires an active WAGER_PARTICIPANT_ROLE membership.

const ROLE = ethers.keccak256(ethers.toUtf8Bytes("WAGER_PARTICIPANT_ROLE"));

// Check status: tier 0 = None, 1 = Bronze … 4 = Platinum
const m = await membership.getMembership(userAddress, ROLE);
const isActive = m.tier > 0 && m.expiresAt > Math.floor(Date.now() / 1000);

// Purchase (USDC approval first). Tier prices come from getTierConfig.
const cfg = await membership.getTierConfig(ROLE, 1 /* Bronze */);
await (await usdc.approve(MEMBERSHIP_MANAGER_ADDRESS, cfg.priceUSDC)).wait();
await (await membership.purchaseTierWithTerms(ROLE, 1, acceptedTermsHash)).wait();

Membership.activeCount and monthCount track your concurrent and monthly usage against the tier's LimitscreateWager reverts once either limit is hit.

Creating a wager

const stake = ethers.parseUnits("10", 6);
const now = Math.floor(Date.now() / 1000);

// 1. Approve the registry for your stake
await (await usdc.approve(WAGER_REGISTRY_ADDRESS, stake)).wait();

// 2. Create — your stake moves into escrow
const tx = await registry.createWager(
    opponentAddress,            // who can accept
    ethers.ZeroAddress,         // arbitrator (required for ThirdParty type)
    USDC_ADDRESS,               // stake token (must pass isAllowedToken)
    stake,                      // creatorStake
    stake,                      // opponentStake (differs for Offer odds)
    now + 6 * 3600,             // acceptDeadline  (≤ 30 days out)
    now + 86400 + 48 * 3600,    // resolveDeadline (≤ 180 days out)
    1,                          // ResolutionType.Creator ("Me" — UI no longer offers Either)
    ethers.ZeroHash,            // oracle conditionId (oracle types only)
    true,                       // creatorIsYes (oracle types only)
    metadataHash,               // keccak256 of the terms
    "ipfs://<cid>"              // terms location (optionally encrypted)
);
const receipt = await tx.wait();

// wagerId from the WagerCreated event
const event = receipt.logs
    .map(l => { try { return registry.interface.parseLog(l); } catch { return null; } })
    .find(e => e?.name === "WagerCreated");
const wagerId = event.args.wagerId;

ResolutionType: 0 Either, 1 Creator, 2 Opponent, 3 ThirdParty, 4 Polymarket, 5 ChainlinkDataFeed, 6 ChainlinkFunctions, 7 UMA.

For oracle types, pass the registered condition ID and which side you're taking (creatorIsYes). For ThirdParty, pass a non-zero arbitrator. createWagerWithTerms(...) additionally binds the accepted terms-version hash on-chain.

Accepting, declining, cancelling

// Opponent: approve their stake, then accept (wager goes Open → Active)
await (await usdc.approve(WAGER_REGISTRY_ADDRESS, opponentStake)).wait();
await (await registry.acceptWager(wagerId)).wait();

// Opponent can reject (creator refunded immediately)
await registry.declineWager(wagerId);

// Creator can withdraw an un-accepted offer
await registry.cancelOpen(wagerId);

// Anyone can sweep stale open offers past their acceptDeadline
await registry.batchExpireOpen([id1, id2, id3]);

Both createWager and acceptWager screen the participants through SanctionsGuard — a SanctionedAddress revert means the address is deny-listed or flagged by the Chainalysis oracle.

Resolving

// Participant / arbitrator resolution (per the wager's ResolutionType)
await registry.declareWinner(wagerId, winnerAddress);

// Draw: first call records consent, the matching call from the other
// party settles it and returns each side's own stake
await registry.declareDraw(wagerId);
await registry.revokeDraw(wagerId);              // back out before the other consents
const { creatorAgreed, opponentAgreed } = await registry.drawConsent(wagerId);

// Oracle settlement — permissionless once the source has resolved
await registry.autoResolveFromPolymarket(wagerId);  // ResolutionType.Polymarket
await registry.autoResolveFromOracle(wagerId);      // Chainlink / UMA types

Claiming and refunds

// Winner pulls the full pot (once)
await registry.claimPayout(wagerId);

// Refunds — Open past acceptDeadline refunds the creator;
// Active past resolveDeadline refunds both sides
await registry.claimRefund(wagerId);

Reading wagers

const w = await registry.getWager(wagerId);
// w.status: 0 None, 1 Open, 2 Active, 3 Resolved, 4 Cancelled, 5 Refunded, 6 Draw
// w.creator / w.opponent / w.arbitrator / w.winner
// w.creatorStake / w.opponentStake (uint128, token decimals)
// w.acceptDeadline / w.resolveDeadline (unix seconds)
// w.metadataHash / w.metadataUri  — terms hash + IPFS pointer

// Paginated per-user queries
const count = await registry.getUserWagerCount(user);
const ids = await registry.getUserWagerIds(user, 0, 50);
const wagers = await registry.getUserWagers(user, 0, 50);

Listening to events

registry.on(registry.filters.WagerCreated(null, creatorAddress), (id, creator, opponent) => {
    console.log(`wager ${id}: ${creator} vs ${opponent}`);
});

Lifecycle events, in order of a typical happy path: WagerCreatedWagerAcceptedWagerResolvedPayoutClaimed. Other exits: WagerCancelled, WagerDeclined, WagerRefunded, DrawProposed/DrawRevoked/WagerDrawn. Oracle links emit PolymarketLinked / OracleConditionLinked at creation. Moderation emits AccountFrozen / AccountUnfrozen.

Encryption keys

For end-to-end encrypted terms, participants publish keys in KeyRegistry:

const keyRegistry = new ethers.Contract(KEY_REGISTRY_ADDRESS, KeyRegistryABI, signer);

if (!(await keyRegistry.hasKey(opponentAddress))) {
    // opponent must registerKey() before you can encrypt a wager for them
}
await keyRegistry.registerKey(publicKeyBytes);            // 32–2048 bytes
const pk = await keyRegistry.getPublicKey(opponentAddress);

The envelope format (X-Wing hybrid KEM + ChaCha20-Poly1305) is specified in the Envelope Encryption Spec.

Common errors

Revert Cause
SanctionedAddress(account) Address deny-listed or flagged by the sanctions oracle
Membership-related revert on create No active tier, or monthly/concurrent limit reached
ERC-20 transferFrom failure Missing/insufficient USDC approval or balance
acceptWager revert Wrong address (named opponent only), deadline passed, or not Open
declareWinner revert Caller not authorized for the wager's resolution type, or wager not Active
claimPayout revert Caller is not the winner, or already paid
claimRefund revert Relevant deadline hasn't passed yet
Frozen-account revert Address frozen by an Account Moderator (isFrozen(user))