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>
231 lines
12 KiB
Text
231 lines
12 KiB
Text
pragma cashscript ^0.12.0;
|
|
|
|
// ParimutuelToteOpenMBA — open-entry, covenant-enforced pari-mutuel tote
|
|
// DENOMINATED IN AN MBA (ADR-0024 Tier 2; ADR-0025). The MBA-staking sibling of
|
|
// the plain-BCH ParimutuelToteOpen: identical state-machine + receipt logic, but
|
|
// the pool holds a backed Deviant asset (mbaCategory FT) instead of native sats.
|
|
//
|
|
// TWO co-spent covenant UTXOs (CashTokens = one category per UTXO; the staked MBA
|
|
// and the tote's own NFTs are different categories):
|
|
// - STATE UTXO (input 0) — toteCategory minting NFT, no FT. 26-byte commitment
|
|
// ledger: phase(1) ++ outcome(1) ++ A(8) ++ B(8) ++ rws(8) [units = MBA units]
|
|
// BET (phase 0): A=totalStake side0, B=totalStake side1.
|
|
// CLAIM (phase 1): A=totalPool (frozen), B=totalWinStake (frozen), rws decrements.
|
|
// - VAULT UTXO (input 1) — mbaCategory FT == totalPool, peg-backed value==amount*rate.
|
|
// Every bet/attest/claim/refund/sweep spends BOTH and is validated by fixed index,
|
|
// so the covenant runs identically whichever of its two inputs triggers it.
|
|
//
|
|
// - RECEIPT NFT — toteCategory MUTABLE NFT minted to the bettor on bet():
|
|
// claimed(1) ++ side(1) ++ stake(8). claimed flips to 1 on claim (double-claim guard).
|
|
//
|
|
// Peg: every mbaCategory output value == tokenAmount * rate. Denomination: rate is
|
|
// sats per MBA base unit — choose it finely for high-value MBAs (ADR-0025) so the
|
|
// floor pro-rata residual (bounded by one base unit) is negligible.
|
|
|
|
contract ParimutuelToteOpenMBA(
|
|
bytes toteCategory, // tote's own NFT category (state + receipts), consensus order
|
|
bytes mbaCategory, // staked MBA/DEA category, consensus order
|
|
int rate, // sats per MBA base unit; every MBA output value == amount*rate
|
|
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 (peg MBA) at attest
|
|
) {
|
|
// ---- BET: fold an MBA stake into the vault; mint the bettor's receipt. ----
|
|
function bet(int addStake, bytes side) {
|
|
require(addStake > 0);
|
|
require(side == 0x00 || side == 0x01);
|
|
require(tx.inputs[0].tokenCategory == toteCategory + 0x02); // state minting NFT
|
|
require(tx.inputs[1].tokenCategory == mbaCategory); // MBA vault
|
|
|
|
bytes phaseB, bytes r1 = tx.inputs[0].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;
|
|
int newPool = tx.inputs[1].tokenAmount + addStake;
|
|
|
|
// [0] state UTXO back: minting NFT + commitment updated, no FT.
|
|
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].tokenAmount == 0);
|
|
// [1] vault UTXO back: grown by addStake, peg held.
|
|
require(tx.outputs[1].lockingBytecode == tx.inputs[1].lockingBytecode);
|
|
require(tx.outputs[1].tokenCategory == mbaCategory);
|
|
require(tx.outputs[1].tokenAmount == newPool);
|
|
require(tx.outputs[1].value == newPool * rate);
|
|
// [2] receipt: mutable toteCategory NFT to the bettor.
|
|
require(tx.outputs[2].tokenCategory == toteCategory + 0x01);
|
|
require(tx.outputs[2].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);
|
|
require(tx.inputs[0].tokenCategory == toteCategory + 0x02);
|
|
require(tx.inputs[1].tokenCategory == mbaCategory);
|
|
|
|
bytes phaseB, bytes r1 = tx.inputs[0].nftCommitment.split(1);
|
|
require(phaseB == 0x00);
|
|
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 (MBA units)
|
|
int netPool = grossPool - rake;
|
|
int totalWinStake = sideA;
|
|
if (outcome == 0x01) { totalWinStake = sideB; }
|
|
require(totalWinStake > 0);
|
|
|
|
// Store NET totals -> claim()/refund()/sweep() split netPool unchanged.
|
|
bytes newCommit = 0x01 + outcome + bytes8(netPool) + bytes8(totalWinStake) + bytes8(totalWinStake);
|
|
|
|
// [0] state back: frozen to CLAIM phase.
|
|
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].tokenAmount == 0);
|
|
// [1] vault: FT reduced by the rake, peg held.
|
|
require(tx.outputs[1].lockingBytecode == tx.inputs[1].lockingBytecode);
|
|
require(tx.outputs[1].tokenCategory == mbaCategory);
|
|
require(tx.outputs[1].tokenAmount == tx.inputs[1].tokenAmount - rake);
|
|
require(tx.outputs[1].value == (tx.inputs[1].tokenAmount - rake) * rate);
|
|
// [2] operator rake as a peg-correct MBA FT output, when non-zero.
|
|
if (rake > 0) {
|
|
require(rake * rate >= 546);
|
|
require(tx.outputs[2].lockingBytecode == new LockingBytecodeP2PKH(rakePkh));
|
|
require(tx.outputs[2].tokenCategory == mbaCategory);
|
|
require(tx.outputs[2].tokenAmount == rake);
|
|
require(tx.outputs[2].value == rake * rate);
|
|
}
|
|
}
|
|
|
|
// ---- CLAIM: winner spends state[0] + vault[1] + own receipt[2]. ----
|
|
function claim(bytes claimerLockingBytecode) {
|
|
require(tx.inputs[0].tokenCategory == toteCategory + 0x02);
|
|
require(tx.inputs[1].tokenCategory == mbaCategory);
|
|
require(tx.inputs[2].tokenCategory == toteCategory + 0x01); // mutable receipt
|
|
|
|
bytes phaseB, bytes r1 = tx.inputs[0].nftCommitment.split(1);
|
|
require(phaseB == 0x01);
|
|
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);
|
|
|
|
bytes rcClaimed, bytes rc1 = tx.inputs[2].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[1].tokenAmount;
|
|
bytes spentReceipt = 0x01 + rcSide + rcStakeB;
|
|
|
|
if (remainingWinStake - stake == 0) {
|
|
// LAST winner: drain the whole MBA vault (absorbs residual); state NFT burned.
|
|
require(tx.outputs[0].lockingBytecode == claimerLockingBytecode);
|
|
require(tx.outputs[0].tokenCategory == mbaCategory);
|
|
require(tx.outputs[0].tokenAmount == poolIn);
|
|
require(tx.outputs[0].value == poolIn * rate);
|
|
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] state back, rws decremented
|
|
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].tokenAmount == 0);
|
|
// [1] vault reduced by payout, peg held
|
|
require(tx.outputs[1].lockingBytecode == tx.inputs[1].lockingBytecode);
|
|
require(tx.outputs[1].tokenCategory == mbaCategory);
|
|
require(tx.outputs[1].tokenAmount == poolIn - payout);
|
|
require(tx.outputs[1].value == (poolIn - payout) * rate);
|
|
// [2] payout MBA to the claimer, peg held
|
|
require(tx.outputs[2].lockingBytecode == claimerLockingBytecode);
|
|
require(tx.outputs[2].tokenCategory == mbaCategory);
|
|
require(tx.outputs[2].tokenAmount == payout);
|
|
require(tx.outputs[2].value == payout * rate);
|
|
// [3] spent receipt (mutable, claimed = 1)
|
|
require(tx.outputs[3].tokenCategory == toteCategory + 0x01);
|
|
require(tx.outputs[3].nftCommitment == spentReceipt);
|
|
}
|
|
}
|
|
|
|
// ---- REFUND: no attest by sweepDeadline -> a bettor reclaims own stake. ----
|
|
function refund(bytes claimerLockingBytecode) {
|
|
require(tx.time >= sweepDeadline);
|
|
require(tx.inputs[0].tokenCategory == toteCategory + 0x02);
|
|
require(tx.inputs[1].tokenCategory == mbaCategory);
|
|
require(tx.inputs[2].tokenCategory == toteCategory + 0x01);
|
|
|
|
bytes phaseB, bytes rest = tx.inputs[0].nftCommitment.split(1);
|
|
require(phaseB == 0x00);
|
|
require(rest.length == 25);
|
|
|
|
bytes rcClaimed, bytes rc1 = tx.inputs[2].nftCommitment.split(1);
|
|
bytes rcSide, bytes rcStakeB = rc1.split(1);
|
|
require(rcClaimed == 0x00);
|
|
int stake = int(rcStakeB);
|
|
int poolIn = tx.inputs[1].tokenAmount;
|
|
bytes spentReceipt = 0x01 + rcSide + rcStakeB;
|
|
|
|
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].tokenAmount == 0);
|
|
require(tx.outputs[1].lockingBytecode == tx.inputs[1].lockingBytecode);
|
|
require(tx.outputs[1].tokenCategory == mbaCategory);
|
|
require(tx.outputs[1].tokenAmount == poolIn - stake);
|
|
require(tx.outputs[1].value == (poolIn - stake) * rate);
|
|
require(tx.outputs[2].lockingBytecode == claimerLockingBytecode);
|
|
require(tx.outputs[2].tokenCategory == mbaCategory);
|
|
require(tx.outputs[2].tokenAmount == stake);
|
|
require(tx.outputs[2].value == stake * rate);
|
|
require(tx.outputs[3].tokenCategory == toteCategory + 0x01);
|
|
require(tx.outputs[3].nftCommitment == spentReceipt);
|
|
}
|
|
|
|
// ---- SWEEP: unclaimed vault remainder -> sweepPkh after sweepDeadline. ----
|
|
function sweep() {
|
|
require(tx.time >= sweepDeadline);
|
|
require(tx.inputs[0].tokenCategory == toteCategory + 0x02);
|
|
require(tx.inputs[1].tokenCategory == mbaCategory);
|
|
|
|
bytes phaseB, bytes rest = tx.inputs[0].nftCommitment.split(1);
|
|
require(phaseB == 0x01);
|
|
require(rest.length == 25);
|
|
|
|
int poolIn = tx.inputs[1].tokenAmount;
|
|
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(sweepPkh));
|
|
require(tx.outputs[0].tokenCategory == mbaCategory);
|
|
require(tx.outputs[0].tokenAmount == poolIn);
|
|
require(tx.outputs[0].value == poolIn * rate);
|
|
}
|
|
}
|