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>
122 lines
6.5 KiB
Text
122 lines
6.5 KiB
Text
pragma cashscript ^0.12.0;
|
|
|
|
// BinaryShareVaultBCH — Family D (ADR-0030) config 1: a pure-BCH collateralized
|
|
// binary share market ("split / merge", the Polymarket model). FEASIBILITY SPIKE.
|
|
// (wiki/event-prediction-and-tote.md §2.3, roadmap item 10.)
|
|
//
|
|
// Collateral is native BCH held in a single VAULT UTXO that also carries the
|
|
// category's MINTING NFT. Outcome shares are IMMUTABLE NFTs of the same category,
|
|
// distinguished by a 1-byte side tag in the commitment:
|
|
// YES = 0x00 ++ amount(8) NO = 0x01 ++ amount(8) [amount in satoshis]
|
|
//
|
|
// Invariant (the whole point): vault sats == Σ outstanding (YES,NO) pairs.
|
|
// split locks `amount` sats and mints a matching YES+NO pair;
|
|
// merge burns a matching YES+NO pair and releases `amount` sats;
|
|
// attest binds the winning outcome to the named oracle (checkDataSig);
|
|
// redeem pays the WINNING share 1:1 for its backing sats — the loser is worthless.
|
|
// Conservation falls out for free: every YES has a complementary NO, total YES ==
|
|
// total NO == total locked, so paying every winner exactly drains the collateral —
|
|
// no per-claim accounting, no pro-rata, NO integer-division residual (unlike the tote).
|
|
//
|
|
// SPIKE SCOPE: shares are per-position NFTs, not fungible FTs, so they cannot be
|
|
// aggregated/subdivided. The fungible-share question is the production open item
|
|
// (§2.3). Trading a position = ASM-swapping its share NFT. A ~dust seed sits in the
|
|
// vault to keep the NFT UTXO above the dust limit; it is the one stranded wart.
|
|
//
|
|
// Vault commitment ledger (2 bytes): phase(1) ++ outcome(1)
|
|
// OPEN == 0x0000 : split / merge allowed.
|
|
// ATTESTED == 0x01 ++ outcome : redeem allowed.
|
|
// Capability suffix: category ++ 0x02 = minting (vault); bare category = immutable (share).
|
|
|
|
contract BinaryShareVaultBCH(
|
|
bytes shareCategory, // category of the vault minting NFT + the share NFTs (consensus order)
|
|
pubkey oraclePk, // result-oracle (named / published)
|
|
bytes eventId, // 16-byte market id, bound into the oracle message
|
|
int attestDeadline // CLTV: attest may not happen before this height
|
|
) {
|
|
// ---- SPLIT: lock `amount` sats; mint a YES+NO share pair to the caller. ----
|
|
function split(int amount) {
|
|
require(this.activeInputIndex == 0);
|
|
require(amount > 0);
|
|
require(tx.inputs[0].nftCommitment == 0x0000); // OPEN
|
|
|
|
// [0] vault back: same minting NFT + commitment, sats grown by `amount`.
|
|
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
|
|
require(tx.outputs[0].tokenCategory == tx.inputs[0].tokenCategory);
|
|
require(tx.outputs[0].nftCommitment == 0x0000);
|
|
require(tx.outputs[0].value == tx.inputs[0].value + amount);
|
|
// [1] YES share, [2] NO share — both immutable NFTs of shareCategory.
|
|
require(tx.outputs[1].tokenCategory == shareCategory);
|
|
require(tx.outputs[1].nftCommitment == 0x00 + bytes8(amount));
|
|
require(tx.outputs[2].tokenCategory == shareCategory);
|
|
require(tx.outputs[2].nftCommitment == 0x01 + bytes8(amount));
|
|
}
|
|
|
|
// ---- MERGE: burn a matching YES+NO pair; release `amount` sats. ----
|
|
// vault = input 0; YES = input 1; NO = input 2.
|
|
function merge(bytes claimerLockingBytecode) {
|
|
require(this.activeInputIndex == 0);
|
|
require(tx.inputs[0].nftCommitment == 0x0000); // only while OPEN
|
|
|
|
require(tx.inputs[1].tokenCategory == shareCategory);
|
|
require(tx.inputs[2].tokenCategory == shareCategory);
|
|
bytes yesSide, bytes yesAmtB = tx.inputs[1].nftCommitment.split(1);
|
|
bytes noSide, bytes noAmtB = tx.inputs[2].nftCommitment.split(1);
|
|
require(yesSide == 0x00);
|
|
require(noSide == 0x01);
|
|
require(yesAmtB == noAmtB); // matching pair
|
|
int amount = int(yesAmtB);
|
|
|
|
// [0] vault back unchanged commitment, sats reduced by `amount`; pair burned.
|
|
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
|
|
require(tx.outputs[0].tokenCategory == tx.inputs[0].tokenCategory);
|
|
require(tx.outputs[0].nftCommitment == 0x0000);
|
|
require(tx.outputs[0].value == tx.inputs[0].value - amount);
|
|
// [1] released collateral (plain BCH) to the caller.
|
|
require(tx.outputs[1].lockingBytecode == claimerLockingBytecode);
|
|
require(tx.outputs[1].value == amount);
|
|
require(tx.outputs[1].tokenCategory == 0x);
|
|
}
|
|
|
|
// ---- ATTEST: oracle fixes the winning outcome; flip OPEN -> ATTESTED. ----
|
|
function attest(datasig oracleSig, bytes message) {
|
|
require(this.activeInputIndex == 0);
|
|
require(tx.time >= attestDeadline);
|
|
require(tx.inputs[0].nftCommitment == 0x0000); // not yet attested
|
|
|
|
require(message.length == 17);
|
|
bytes msgEventId, bytes outcome = message.split(16);
|
|
require(msgEventId == eventId);
|
|
require(outcome == 0x00 || outcome == 0x01);
|
|
require(checkDataSig(oracleSig, message, oraclePk));
|
|
|
|
// [0] vault back: phase -> ATTESTED, outcome stored; sats unchanged.
|
|
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
|
|
require(tx.outputs[0].tokenCategory == tx.inputs[0].tokenCategory);
|
|
require(tx.outputs[0].nftCommitment == 0x01 + outcome);
|
|
require(tx.outputs[0].value == tx.inputs[0].value);
|
|
}
|
|
|
|
// ---- REDEEM: winning share -> 1:1 sats; loser worthless (rejected). ----
|
|
// vault = input 0; winning share = input 1.
|
|
function redeem(bytes claimerLockingBytecode) {
|
|
require(this.activeInputIndex == 0);
|
|
bytes phaseB, bytes outcomeB = tx.inputs[0].nftCommitment.split(1);
|
|
require(phaseB == 0x01); // ATTESTED
|
|
|
|
require(tx.inputs[1].tokenCategory == shareCategory);
|
|
bytes side, bytes amtB = tx.inputs[1].nftCommitment.split(1);
|
|
require(side == outcomeB); // winner only
|
|
int amount = int(amtB);
|
|
|
|
// [0] vault back: commitment unchanged, sats reduced by `amount`; share burned.
|
|
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 == tx.inputs[0].value - amount);
|
|
// [1] payout (plain BCH) to the winner.
|
|
require(tx.outputs[1].lockingBytecode == claimerLockingBytecode);
|
|
require(tx.outputs[1].value == amount);
|
|
require(tx.outputs[1].tokenCategory == 0x);
|
|
}
|
|
}
|