# Durian Entropy SDK — integrate provably-fair on-chain randomness on KUB

> **Proprietary — © 2026 Durian (durianfun). All rights reserved. NOT open source.** This SDK is
> licensed only to integrate with the canonical Durian Entropy deployment (see `/LICENSE`). No fork,
> redistribution, sublicensing, or use with any other/competing deployment.

Randomness-as-a-Service on KUB Chain. Your contract requests randomness; a Durian keeper (or anyone)
settles it; your contract gets a callback. The user signs **once**; you pay a small gas-scaled fee.

> **Honest framing:** this is **blockhash commit–reveal**, not a VRF. A block producer can bias a
> single block, so any *money* consumer MUST cap the aggregate value resting on one reveal block
> (see §5). It is provably-fair (recomputable from chain data), not cryptographically unbiasable.

---

## 0. Addresses & config

| | Value |
|---|---|
| Chain | KUB (Bitkub), `chainId 96` (`0x60`), RPC `https://rpc.bitkubchain.io` |
| DurianEntropyV2 | `0xE2BACB42Ce5C5C1FB32335ac06B79bE28fb54caB` (live on KUB; V1 primitive at `0xf2DF…754E`) |
| Keeper API | `https://durian-entropy-keeper.pupkaikub.workers.dev/reveal` |
| Explorer | `https://www.kubscan.com` |

Three pieces ship in this repo:
- **Solidity SDK** — `contracts/EntropyConsumer.sol` + `contracts/IEntropyConsumer.sol` (inherit, write 1 fn).
- **Frontend SDK** — `sdk/durian-entropy.js` (ethers v6; request → settle → user fallback).
- **Keeper API** — `keeper/worker.js` (the Durian-run Cloudflare Worker; you just call it, see §3).

---

## 1. On-chain integration (Solidity) — the whole thing is ~10 lines

Inherit `EntropyConsumer`, request with `_roll`, and implement `_onRandomness`:

```solidity
import "./EntropyConsumer.sol";
import "./EntropyLib.sol"; // optional: derive many values from one reveal

contract MyGame is EntropyConsumer {
    using EntropyLib for uint256;
    uint32 constant CB_GAS = 150_000;
    mapping(uint256 => address) public player;

    constructor(address entropyV2) EntropyConsumer(entropyV2) {}

    // user signs ONCE here; pass the protocol fee through (read it with entropy.fee(CB_GAS))
    function play() external payable {
        require(msg.value >= entropy.fee(CB_GAS), "fee");
        uint256 id = _roll(keccak256(abi.encodePacked(msg.sender, block.number)), CB_GAS);
        player[id] = msg.sender;
        // refund any overpay: if (msg.value > f) send back msg.value - f;
    }

    // called once, later, when the request is revealed (by a keeper / the next user / anyone)
    function _onRandomness(uint256 id, uint256 rng) internal override {
        address p = player[id];
        uint256 pocket = rng.pick(0, 37); // 0..36, domain-separated
        // ... settle / pay out / mint ...
    }
}
```

What the base gives you (you don't write any of it):
- `_roll(seed, cbGas)` — pays `entropy.fee(cbGas)`, requests a callback, and records the id as **yours**
  (`known[id]`) so a spoofed callback for a foreign id can never trigger your `_onRandomness`.
- `onRandomness` — gated to `msg.sender == entropy` **and** `known[id]`, single-delivery (`delivered[id]`).
- `settle(id)` — anyone can poke this if the auto-callback ever failed; delivers the stored result once.
- `_tryReveal(id)` / `_trySeal(id)` — self-crank helpers (settle the *previous* request on a new action).

### Make the UX nicer + safer (recommended, from `examples/`)
- **Self-crank (sign-once for everyone):** in your `play()`, settle the *previous* pending request
  before starting a new one — `if (last != SENTINEL) _tryReveal(last); last = id;`. The next player
  settles the previous one for free, so users almost never sign a second tx. (See RandomTierNFT /
  RouletteConsumer / CoinFlipHouse.)
- **Refund overpay:** `_roll` forwards exactly `entropy.fee(cbGas)`. If you accept `msg.value > fee`,
  refund the difference: `if (msg.value > f) { (bool ok,) = msg.sender.call{value: msg.value-f}(""); require(ok); }`.
- **Pay winners with a pull ledger,** never a `.call` inside `_onRandomness` (a reverting receiver
  would strand your state). Credit `winnings[player] += payout` and add a `claim()`. (See CoinFlipHouse.)

### Picking `cbGas` (gas forwarded to YOUR `_onRandomness`)
| your callback does | suggested cbGas |
|---|---|
| record/read a value (roulette pocket) | 80k – 120k |
| coinflip payout via pull ledger | 120k – 180k |
| NFT `_safeMint` + storage | 180k – 250k |

Too low → the on-reveal callback fails (caught), and `settle(id)` recovers it in one extra tx (no fund
loss). The fee scales with `cbGas`, so don't over-allocate. Ceiling: 8,000,000.

### Core functions you may call directly
| fn | purpose |
|---|---|
| `request(bytes32 seed) → id` | free, no callback (settle yourself) |
| `requestCallback(seed, consumer, cbGas) payable → id` | pay fee, get a callback (what `_roll` calls) |
| `fee(uint32 cbGas) → uint256` | the fee to send, at the current gas price |
| `seal(id)` / `sealBatch(ids)` | capture the blockhash within 256 blocks (cheap; enables claim-anytime) |
| `reveal(id) → rng` / `revealBatch(ids)` | compute result + fire callback (anytime once sealed) |
| `status(id) → uint8` | `0 NONE · 1 WAITING · 2 READY · 3 EXPIRED · 4 FULFILLED` |
| `results(id) → uint256` | stored randomness (0 until revealed) |

---

## 2. Frontend integration (JS) — request, then settle with a user fallback

```js
import { DurianEntropy } from "./durian-entropy.js"; // ethers v6 in the page

const entropy = new DurianEntropy({
  address: "0xE2BACB42Ce5C5C1FB32335ac06B79bE28fb54caB",
  workerUrl: "https://durian-entropy-keeper.pupkaikub.workers.dev/reveal",
  provider, // ethers read provider / BrowserProvider
});

// 1) user signs ONE tx — your dapp's play()/mint() (which calls _roll under the hood)
const receipt = await (await myGame.connect(signer).play({ value: await entropy.fee(150000) })).wait();
const id = entropy.idFromReceipt(receipt);

// 2) settle: pings the Durian keeper, watches for the result, and if the keeper is silent,
//    falls back to a USER-SIGNED seal+reveal so it always settles within the window.
const rng = await entropy.settle(id, {
  signer,
  onState: (s) => console.log(s), // 'settling' → ('fallback'→'sealing'→'revealing'→) 'done'
});
```

### Frontend SDK API
| method | returns | notes |
|---|---|---|
| `new DurianEntropy({address, workerUrl, provider})` | — | workerUrl optional |
| `fee(cbGas)` | `Promise<bigint>` | fee to send for a request |
| `status(id)` / `results(id)` / `sealedHash(id)` | `Promise` | reads |
| `idFromReceipt(receipt)` | `bigint` | parse the `Requested` event |
| `pingWorker(id)` | — | fire-and-forget keeper ping (best-effort) |
| `settle(id, {signer, onState, timeoutMs=45000, pollMs=4000})` | `Promise<bigint>` | **the locked pattern**: keeper-first, user-fallback (onState also gets `{elapsedMs, remainingMs}`) |
| `userSettle(id, signer, onState)` | `Promise` | the fallback path alone (seal if needed → reveal) |
| `requestCallback(signer, {seed, consumer, cbGas})` | `Promise<{id, receipt}>` | Pattern B: drive entropy directly |
| `Status` | `{NONE,WAITING,READY,EXPIRED,FULFILLED}` | enum |

> **Every dapp MUST keep the fallback.** `settle()` already does it: if the Durian keeper is down,
> your user signs the reveal themselves. Never rely on the keeper alone.

---

## 3. Keeper API (the "API call") — `POST /reveal`

The Durian keeper is a Cloudflare Worker. Your frontend pings it right after the request tx; it
**settles on-ping as soon as the reveal block is mined (~6–9s)** (the cron sweep is the backstop), so
the user rarely needs the fallback. It is **gated**: it verifies the id on-chain before acting, so it
can't be spammed into wasting gas.

**Request**
```
POST https://durian-entropy-keeper.pupkaikub.workers.dev/reveal
Content-Type: application/json

{ "id": "<request id as decimal string>" }
```

**Responses**
| status | body | meaning |
|---|---|---|
| 200 | `{ "ok": true, "queued": "<id>" }` | accepted; will be sealed+revealed on the next cron tick |
| 200 | `{ "ok": true, "note": "already fulfilled" }` | nothing to do |
| 404 | `{ "error": "no such request" }` | id doesn't exist on-chain (rejected, no gas spent) |
| 400 | `{ "error": "bad id" \| "bad json" }` | malformed |
| 502 | `{ "error": "rpc" }` | transient RPC issue; retry |

You normally never call this directly — `entropy.settle()` (frontend SDK) calls it for you. CORS is
open (`POST`, `OPTIONS`). The keeper holds only a small gas float; settlement liveness never depends
on it (self-crank, user fallback, and expiry-forfeit all back it up).

---

## 4. Fees

`fee(cbGas) = markupBps/10000 × (baseGas + cbGas) × max(tx.gasprice, minGasPrice)`.

Roughly **one reveal's gas, ×markup** — read it live and send it with the request. Overpay is safe
(the SDK base forwards exactly the fee; refund the rest in your consumer). The fee funds the keeper +
the Durian treasury. Free, self-served path: use `request`/`reveal` (no callback, no fee), or run your
own keeper. Measured (25 gwei): `requestCallback`+fee ≈136k gas, `seal` ≈48k, `reveal` ≈152k.

---

## 5. Rules every MONEY consumer MUST follow (not optional)

1. **Escrow at request time.** Take the stake/intent in the same tx as `_roll`. A loser who refuses to
   settle then gains nothing (the stake is already yours; unsettled → expiry forfeits it).
2. **Cap the AGGREGATE value per reveal block** — not just per bet. A block producer can bias one
   blockhash and skew every bet settling on that block at once. Track
   `committedAtBlock[block.number+1] += stake; require(... <= perBlockCap)` and size `perBlockCap` so a
   grinder's expected gain stays well under a KUB block reward (target EV < block_reward / 10). In
   `CoinFlipHouse` this is a **constructor parameter (immutable)** on purpose — you MUST choose a value
   for KUB's real block reward; do not copy a placeholder. On KUB this is a SMALL number.
3. **Never let a pre-reveal animation decide value.** The only source of truth is the callback / the
   on-chain `results(id)`. Show a cosmetic spin immediately, ease to the real result on the event.
4. **Pay winners via a pull ledger**, never a push `.call` inside the callback (a reverting receiver
   would strand your accounting). Credit `winnings[player]` and add a `claim()`. See CoinFlipHouse.
5. **On expiry, forfeit — never refund-and-re-roll.** Re-rolling is a grinding exploit.

---

## 6. Lifecycle & errors

```
request ──(block N+1 mined)──> WAITING ──> READY ──(seal within 256 blk)──> READY (forever) ──> FULFILLED
                                              └─(no seal, 256 blk pass)──> EXPIRED (forfeit)
```
Reverts: `too early` (before the reveal block), `expired` (256-block window passed unsealed),
`no request` (unknown id), `fee` (underpaid), `cbGas too high` (> 8,000,000).

Examples to copy: `contracts/examples/{CoinFlipHouse,RandomTierNFT,RouletteConsumer}.sol`.
Full design + security: `docs/ENTROPY_V2_PLAN.md`.
