SDK & reference

Integrate in minutes.

Every function, every event, and copy-paste Solidity + JavaScript. Point at the deployed, verified contract and you have provably-fair randomness on KUB. No package to install for on-chain use.

Quickstart

Two calls, across two blocks. request commits; reveal settles once the future block is mined.

flow.sol
uint256 id = entropy.request(seed);   // TX 1 — commit; id from the Requested event
// wait until block.number > revealBlock (poll status() to 2 = READY, ~10s on KUB)
uint256 r  = entropy.reveal(id);       // TX 2 — settle; r is your randomness
The #1 mistake: reveal needs block.number > revealBlock (revealBlock = requestBlock + 1), so it succeeds only at least 2 blocks after request. Poll status(id) to 2 (READY); never reveal one block after request.

Addresses & config

ItemValue
ChainKUB Chain (Bitkub) · chainId 96 (0x60)
RPChttps://rpc.bitkubchain.io · https://rpc-l1.bitkubchain.io — the status-poll loop hits the RPC repeatedly, so use a fallback set (viem fallback() transport, below) so one rate-limited node can't stall it
Contract0xf2DF5c645d84Deb994979d07C0b02410D718754E ✓ verified
Explorerhttps://www.kubscan.com/address/<addr> · /tx/<hash> · /block/<n>
Compilersolc 0.8.24 · optimizer 200 · evm paris

Ownerless and immutable — this address never changes. The core contract is source-available (don't fork/redeploy); the interface, helper, and examples below are MIT.

Contract functions

request(bytes32 seed) → uint256 idnonpayable

Commit. Reserves the next block (revealBlock = block.number + 1) and returns a globally-unique id. Read id from the Requested event. seed is an optional commitment folded into the result (pass bytes32(0) if unused — it adds no entropy, only binds the number to data you commit now).

reveal(uint256 id) → uint256 randomnessnonpayable · permissionless · idempotent

Settle and return the randomness keccak256(blockhash(revealBlock), id, requester, seed). Anyone may call it; a second call on a settled id returns the same number and emits no new event.

Reverts: too early (before the reveal block — this is what blocks atomic grinding), expired (after the 256-block window), no request (unknown id).
status(uint256 id) → uint8view

Lifecycle: 0 NONE · 1 WAITING · 2 READY · 3 EXPIRED · 4 FULFILLED. Poll to 2 before revealing; bail on 3 (a request can go WAITING → EXPIRED without ever showing READY if nobody settles in 256 blocks).

results(uint256 id) → uint256view

The stored randomness for a settled id. Returns 0 for any id that is not FULFILLED.

requests(uint256 id) → (address requester, uint64 revealBlock, bool fulfilled, bytes32 seed)view

The full request record. Use it (or the Requested event) to get requester, revealBlock, and seed for the off-chain recompute.

nextId() → uint256view

The id the next request will receive.

EXPIRY() → uint256view · constant

The reveal window, in blocks. Always 256.

Events

Requested(uint256 indexed id, address indexed requester, uint64 revealBlock, bytes32 seed)

Emitted by request. The canonical source for the new id + the inputs needed to verify.

Revealed(uint256 indexed id, address indexed requester, uint256 randomness)

Emitted once, on the first reveal of an id. A second reveal emits nothing.

EntropyLib helpers (MIT)

One reveal is a full 256-bit number. Derive as many independent, domain-separated values as you need — don't make multiple requests for one event. EntropyLib is an internal pure library you inline into your contract (no external call).

EntropyLib usage
using EntropyLib for uint256;
uint256 r = entropy.reveal(id);

uint256[] memory vals = r.expand(8);   // 8 independent values (n <= 256)
uint256 pocket = r.pick(0, 37);    // roulette 0..36 (max != 0)
uint256 dice   = r.pick(1, 6);     // a d6 — salt 1, independent of pocket
uint256 lotto  = r.digits(6);      // 6 packed digits e.g. 482917 (m <= 77)

digits returns a number, so leading zeros drop (0048214821) — pad when displaying. pick is effectively unbiased for max well below 2^256.

Frontend integration

ethers v6 — full flow

draw.ethers.js
import { Contract, BrowserProvider } from "ethers";
const ENTROPY = "0xf2DF5c645d84Deb994979d07C0b02410D718754E";
const entropy = new Contract(ENTROPY, ABI, await (new BrowserProvider(window.ethereum)).getSigner());
const sleep = ms => new Promise(r => setTimeout(r, ms));

// 1) commit — capture id, requester, seed (needed to verify later)
const seed = "0x" + "00".repeat(32);
const rc = await (await entropy.request(seed)).wait();
const id = rc.logs.map(l => { try { return entropy.interface.parseLog(l); } catch { return null; } })
                  .find(p => p && p.name === "Requested").args.id;

// 2) poll status to READY (2); bail on EXPIRED (3)
let st = Number(await entropy.status(id));
while (st === 1) { await sleep(2500); st = Number(await entropy.status(id)); }
if (st === 3) throw new Error("expired — treat as a loss");

// 3) reveal + read
await (await entropy.reveal(id)).wait();
const r = await entropy.results(id);          // uint256
const fourDigits = (r % 10000n).toString().padStart(4, "0");

viem — same flow

draw.viem.js
import { createPublicClient, createWalletClient, custom, http, fallback, parseEventLogs } from "viem";
const ENTROPY = "0xf2DF5c645d84Deb994979d07C0b02410D718754E", seed = "0x" + "00".repeat(32);
const chain = { id: 96, name: "KUB", nativeCurrency: { name: "KUB", symbol: "KUB", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.bitkubchain.io"] } } };
// fallback() rotates RPCs so the status poll loop survives a rate-limited node
const pub = createPublicClient({ chain, transport: fallback([http("https://rpc.bitkubchain.io"), http("https://rpc-l1.bitkubchain.io")]) });
const wallet = createWalletClient({ chain, transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();

const rc = await pub.waitForTransactionReceipt({ hash: await wallet.writeContract({ account, address: ENTROPY, abi: ABI, functionName: "request", args: [seed] }) });
const [{ args: { id } }] = parseEventLogs({ abi: ABI, eventName: "Requested", logs: rc.logs });

let st = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "status", args: [id] });
while (st === 1) { await new Promise(r => setTimeout(r, 2500)); st = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "status", args: [id] }); }
if (st === 3) throw new Error("expired");

await pub.waitForTransactionReceipt({ hash: await wallet.writeContract({ account, address: ENTROPY, abi: ABI, functionName: "reveal", args: [id] }) });
const r = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "results", args: [id] });

Solidity consumers

Bind the live contract and call two functions. Follow the three rules (below) or you can get drained.

This interface plus the address is the entire on-chain SDK — nothing to install. Save it next to your contract as IDurianEntropy.sol (MIT, free to copy):

IDurianEntropy.sol (MIT)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

interface IDurianEntropy {
    function request(bytes32 seed) external returns (uint256 id);
    function reveal(uint256 id) external returns (uint256 randomness);
    function results(uint256 id) external view returns (uint256 randomness);
    function status(uint256 id) external view returns (uint8); // 0 NONE 1 WAITING 2 READY 3 EXPIRED 4 FULFILLED
}
NftMint.sol — grind-proof random tier
import "./IDurianEntropy.sol";
IDurianEntropy constant entropy = IDurianEntropy(0xf2DF5c645d84Deb994979d07C0b02410D718754E);
struct Pending { uint256 reqId; bool open; }
mapping(address => Pending) public pending;

function commitMint() external payable {            // click 1: pay + lock the roll
    require(msg.value == PRICE);
    require(!pending[msg.sender].open, "pending");
    pending[msg.sender] = Pending(entropy.request(bytes32(uint256(uint160(msg.sender)))), true);
}
function claimMint(address user) external {         // click 2 (2 blocks later): permissionless
    Pending memory p = pending[user];
    require(p.open, "none");
    uint256 r = entropy.reveal(p.reqId);
    delete pending[user];
    _mint(user, pickTier(r));                  // grind-proof: payment already committed
}
function expireMint(address user) external {        // rule 3: clear an expired roll = forfeit, no re-roll
    require(pending[user].open && entropy.status(pending[user].reqId) == 3, "not expired");
    delete pending[user];
}

A full coin-flip reference (escrow, permissionless settle, forfeit) is in the downloadable guide.

Verify any result off-chain (provably fair)

Anyone can recompute a settled result from public chain data. Read requester, revealBlock, seed from requests(id).

verify.viem.js
import { keccak256, encodePacked } from "viem";
const req = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "requests", args: [id] });
const block = await pub.getBlock({ blockNumber: BigInt(req.revealBlock) });
const r = BigInt(keccak256(encodePacked(
  ["bytes32", "uint256", "address", "bytes32"],
  [block.hash, BigInt(id), req.requester, req.seed]
)));   // === results(id)

ABI

DurianEntropy.abi.json
[
  {"type":"function","name":"request","stateMutability":"nonpayable","inputs":[{"name":"seed","type":"bytes32"}],"outputs":[{"name":"id","type":"uint256"}]},
  {"type":"function","name":"reveal","stateMutability":"nonpayable","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"randomness","type":"uint256"}]},
  {"type":"function","name":"results","stateMutability":"view","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},
  {"type":"function","name":"status","stateMutability":"view","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"","type":"uint8"}]},
  {"type":"function","name":"requests","stateMutability":"view","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"requester","type":"address"},{"name":"revealBlock","type":"uint64"},{"name":"fulfilled","type":"bool"},{"name":"seed","type":"bytes32"}]},
  {"type":"function","name":"nextId","stateMutability":"view","inputs":[],"outputs":[{"type":"uint256"}]},
  {"type":"function","name":"EXPIRY","stateMutability":"view","inputs":[],"outputs":[{"type":"uint256"}]},
  {"type":"event","name":"Requested","inputs":[{"name":"id","type":"uint256","indexed":true},{"name":"requester","type":"address","indexed":true},{"name":"revealBlock","type":"uint64","indexed":false},{"name":"seed","type":"bytes32","indexed":false}]},
  {"type":"event","name":"Revealed","inputs":[{"name":"id","type":"uint256","indexed":true},{"name":"requester","type":"address","indexed":true},{"name":"randomness","type":"uint256","indexed":false}]}
]

Status · reverts · gas

status enum

ValueNameWhat to do
0NONEunknown id — nothing
1WAITINGreveal block not passed — keep polling
2READYcall reveal(id)
3EXPIREDforfeit / treat as a loss. Never auto re-roll the same stake
4FULFILLEDread results(id)

revert reasons

StringWhen
no requestunknown id
too earlyreveal before block.number > revealBlock (also blocks atomic grinding)
expiredreveal after the 256-block window
block overflowrequest if block.number+1 > uint64 max (unreachable in practice)

gas (local estimate at 25 gwei — on-chain may vary)

OpGasKUB
request~53,400~0.0013
reveal~53,700~0.0013
request + reveal~107,000~0.0027

Consumer rules

1. Commit at request time. Take payment/intent in the request transaction, before the outcome is knowable.
2. Make settlement permissionless. Let anyone settle inside the 256-block window, so a loser can't dodge by not revealing.
3. Expiry = forfeit. On EXPIRED the requester loses. Never refund-and-re-roll the same stake (a new, independently-funded request is fine).
Cap value. A block producer can bias a single block — the irreducible limit without a real VRF. Size per-round and aggregate caps so manipulation is never worth its cost. Blockhash commit-reveal, not a VRF: provably fair (recomputable), not cryptographically unbiasable.
↓ Guide.md