deviant/contracts/ParimutuelToteOpen.cash
walerikus 518a3ce174 chore: initial project baseline — full Deviant codebase under version control
Brings the whole project into git (previously only the on-ramp feature and
a few parallel files were tracked). Includes: wallet core (src/), CashScript
contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs
corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text).

Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF
key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB),
transcripts/ (session logs), and machine-local config (.claude/*.local,
*.lock). Secret-scanned before commit — no credentials in the tree.

Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are
legacy (OP_RETURN era) but still imported; committed as-is, flagged for a
separate removal once de-referenced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:38:07 +02:00

202 lines
10 KiB
Text

pragma cashscript ^0.12.0;
// ParimutuelToteOpen — open-entry, covenant-ENFORCED pari-mutuel tote in PLAIN BCH
// (ADR-0024 Tier 2, wiki/event-prediction-and-tote.md §3.1). Arbitrary-N dynamic
// roster AND the covenant computes every pro-rata payout — no operator trust for
// the split (cf. OpenTote Tier 1). Outcome rests on a named result-oracle.
//
// Pooled value is NATIVE BCH (satoshis), not a token — so there is ONE token
// category (the tote's own NFTs) and the pool collapses to a SINGLE UTXO: the
// state NFT rides on the same UTXO as the pooled sats. (An MBA/DEA-denominated
// variant re-adds a co-spent FT vault; see the design doc.)
//
// NFTs as covenant machinery (not collectibles):
// - STATE NFT (minting capability) on the pool UTXO. 26-byte commitment ledger:
// phase(1) ++ outcome(1) ++ A(8) ++ B(8) ++ rws(8) [all values in sats]
// BET (phase 0): A=totalStake side0, B=totalStake side1, rws=0.
// CLAIM (phase 1): A=totalPool (frozen), B=totalWinStake (frozen),
// rws=remainingWinStake (decrements per claim).
// The pool's satoshis == the pooled stake (= remaining pool during CLAIM).
// - RECEIPT NFT (mutable) minted to the bettor on bet():
// claimed(1) ++ side(1) ++ stake(8, sats). Marking claimed=1 on claim is
// the double-claim guard.
//
// Capability suffixes: toteCategory ++ 0x02 = minting (state); ++ 0x01 = mutable
// (receipt). The pool is the only covenant input each tx (single-UTXO model).
contract ParimutuelToteOpen(
bytes toteCategory, // tote's own NFT category (state + receipts), consensus order
pubkey oraclePk, // result-oracle (named / published)
bytes eventId, // 16-byte market id, bound into the oracle message
int betDeadline, // CLTV: attest may not happen before this height
int sweepDeadline, // CLTV: refund / sweep may not happen before this height
bytes20 sweepPkh, // recipient of swept (unclaimed) remainder
int rakeBps, // operator takeout, basis points (0 = no rake)
bytes20 rakePkh // operator (house) — receives the rake at attest
) {
// ---- BET: fold a BCH stake into the pool; mint the bettor's receipt. ----
function bet(int addStake, bytes side) {
require(addStake > 0);
require(side == 0x00 || side == 0x01);
bytes phaseB, bytes r1 = tx.inputs[this.activeInputIndex].nftCommitment.split(1);
require(phaseB == 0x00); // betting still open
bytes outcomeB, bytes r2 = r1.split(1);
bytes aB, bytes r3 = r2.split(8);
bytes bB, bytes rwsB = r3.split(8);
int newA = int(aB);
int newB = int(bB);
if (side == 0x00) { newA = newA + addStake; }
else { newB = newB + addStake; }
bytes newCommit = phaseB + outcomeB + bytes8(newA) + bytes8(newB) + rwsB;
// [0] pool UTXO back: minting NFT + commitment updated, sats grown by stake.
require(tx.outputs[0].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
require(tx.outputs[0].nftCommitment == newCommit);
require(tx.outputs[0].value == tx.inputs[this.activeInputIndex].value + addStake);
// [1] receipt: mutable toteCategory NFT to the bettor.
require(tx.outputs[1].tokenCategory == toteCategory + 0x01);
require(tx.outputs[1].nftCommitment == 0x00 + side + bytes8(addStake));
}
// ---- ATTEST: oracle fixes the outcome; freeze totals; flip to CLAIM. ----
function attest(datasig oracleSig, bytes message) {
require(tx.time >= betDeadline);
bytes phaseB, bytes r1 = tx.inputs[this.activeInputIndex].nftCommitment.split(1);
require(phaseB == 0x00); // not yet attested
bytes oldOutcomeB, bytes r2 = r1.split(1);
bytes aB, bytes r3 = r2.split(8);
bytes bB, bytes rwsB = r3.split(8);
require(oldOutcomeB == 0x00);
require(int(rwsB) == 0);
require(message.length == 17);
bytes msgEventId, bytes outcome = message.split(16);
require(msgEventId == eventId);
require(outcome == 0x00 || outcome == 0x01);
require(checkDataSig(oracleSig, message, oraclePk));
int sideA = int(aB);
int sideB = int(bB);
int grossPool = sideA + sideB;
int rake = grossPool * rakeBps / 10000; // operator takeout off the top
int netPool = grossPool - rake;
int totalWinStake = sideA;
if (outcome == 0x01) { totalWinStake = sideB; }
require(totalWinStake > 0); // no-winner market is void -> refund/sweep
// Store NET totals -> claim()/refund()/sweep() (which read totalPool) split netPool.
bytes newCommit = 0x01 + outcome + bytes8(netPool) + bytes8(totalWinStake) + bytes8(totalWinStake);
// [0] pool back: commitment frozen to CLAIM phase; sats reduced by the rake.
require(tx.outputs[0].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
require(tx.outputs[0].nftCommitment == newCommit);
require(tx.outputs[0].value == tx.inputs[this.activeInputIndex].value - rake);
// [1] operator rake (plain BCH), when non-zero. Anti-dust per R-002.
if (rake > 0) {
require(rake >= 546);
require(tx.outputs[1].lockingBytecode == new LockingBytecodeP2PKH(rakePkh));
require(tx.outputs[1].value == rake);
require(tx.outputs[1].tokenCategory == 0x);
}
}
// ---- CLAIM: winner spends pool[0] + own receipt[1]; covenant pays floor
// pro-rata in sats, decrements rws, marks the receipt spent. ----
function claim(bytes claimerLockingBytecode) {
require(this.activeInputIndex == 0); // pool is input 0; receipt is input 1
bytes phaseB, bytes r1 = tx.inputs[0].nftCommitment.split(1);
require(phaseB == 0x01); // claim phase
bytes outcomeB, bytes r2 = r1.split(1);
bytes poolB, bytes r3 = r2.split(8);
bytes winB, bytes rwsB = r3.split(8);
int totalPool = int(poolB);
int totalWinStake = int(winB);
int remainingWinStake = int(rwsB);
require(tx.inputs[1].tokenCategory == toteCategory + 0x01); // mutable receipt
bytes rcClaimed, bytes rc1 = tx.inputs[1].nftCommitment.split(1);
bytes rcSide, bytes rcStakeB = rc1.split(1);
require(rcClaimed == 0x00); // not already claimed
require(rcSide == outcomeB); // winner only
int stake = int(rcStakeB);
int payout = stake * totalPool / totalWinStake; // floor
int poolIn = tx.inputs[0].value;
bytes spentReceipt = 0x01 + rcSide + rcStakeB;
if (remainingWinStake - stake == 0) {
// LAST winner: take the whole remaining pool (absorbs the
// sum-of-floors residual -> no dust stranded); state NFT is burned.
require(tx.outputs[0].lockingBytecode == claimerLockingBytecode);
require(tx.outputs[0].value == poolIn);
require(tx.outputs[0].tokenCategory == 0x); // plain BCH payout
require(tx.outputs[1].tokenCategory == toteCategory + 0x01);
require(tx.outputs[1].nftCommitment == spentReceipt);
} else {
require(remainingWinStake - stake > 0);
bytes newCommit = phaseB + outcomeB + poolB + winB + bytes8(remainingWinStake - stake);
// [0] pool back, rws decremented, sats reduced by payout.
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
require(tx.outputs[0].tokenCategory == tx.inputs[0].tokenCategory);
require(tx.outputs[0].nftCommitment == newCommit);
require(tx.outputs[0].value == poolIn - payout);
// [1] payout (plain BCH) to the claimer.
require(tx.outputs[1].lockingBytecode == claimerLockingBytecode);
require(tx.outputs[1].value == payout);
require(tx.outputs[1].tokenCategory == 0x);
// [2] spent receipt (mutable, claimed = 1).
require(tx.outputs[2].tokenCategory == toteCategory + 0x01);
require(tx.outputs[2].nftCommitment == spentReceipt);
}
}
// ---- REFUND: no attest by sweepDeadline -> a bettor reclaims own stake. ----
function refund(bytes claimerLockingBytecode) {
require(this.activeInputIndex == 0);
require(tx.time >= sweepDeadline);
bytes phaseB, bytes rest = tx.inputs[0].nftCommitment.split(1);
require(phaseB == 0x00); // never attested
require(rest.length == 25);
require(tx.inputs[1].tokenCategory == toteCategory + 0x01);
bytes rcClaimed, bytes rc1 = tx.inputs[1].nftCommitment.split(1);
bytes rcSide, bytes rcStakeB = rc1.split(1);
require(rcClaimed == 0x00);
int stake = int(rcStakeB);
int poolIn = tx.inputs[0].value;
bytes spentReceipt = 0x01 + rcSide + rcStakeB;
// [0] pool back unchanged, sats reduced by the refunded stake.
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
require(tx.outputs[0].tokenCategory == tx.inputs[0].tokenCategory);
require(tx.outputs[0].nftCommitment == tx.inputs[0].nftCommitment);
require(tx.outputs[0].value == poolIn - stake);
// [1] stake (plain BCH) back to the bettor. [2] spent receipt.
require(tx.outputs[1].lockingBytecode == claimerLockingBytecode);
require(tx.outputs[1].value == stake);
require(tx.outputs[1].tokenCategory == 0x);
require(tx.outputs[2].tokenCategory == toteCategory + 0x01);
require(tx.outputs[2].nftCommitment == spentReceipt);
}
// ---- SWEEP: unclaimed pool remainder -> sweepPkh after sweepDeadline. ----
function sweep() {
require(this.activeInputIndex == 0);
require(tx.time >= sweepDeadline);
bytes phaseB, bytes rest = tx.inputs[0].nftCommitment.split(1);
require(phaseB == 0x01); // attested-but-unclaimed pool
require(rest.length == 25);
// pool remainder -> sweepPkh (plain BCH); state NFT burned.
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(sweepPkh));
require(tx.outputs[0].value == tx.inputs[0].value);
require(tx.outputs[0].tokenCategory == 0x);
}
}