Snapshot of the shared working tree's outstanding changes (multiple concurrent workstreams), excluding generated runtime state files (.football-tote-state.json, .tote-platform-state.json, .usdm-historical-chart.html). Spans: wallet shell (connect/, keystore MultiKeyStore+vault, screens, currency picker, sign-message), verify artifacts, new Potidaea/DRA/tote contracts + regtest suites, cross-chain/T6 + WalletConnect Parameters, a telegram-orderbook subproject, design mockups, and docs. See shells/app/WALLET-COORDINATION.md for the per-file ownership split. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
166 lines
8.2 KiB
Text
166 lines
8.2 KiB
Text
pragma cashscript ^0.12.0;
|
||
|
||
// GamePoolEscrowFlatV2 — flat-fee escrow with per-side refund.
|
||
//
|
||
// Identical to GamePoolEscrowFlat (existing artifact) except for ONE new
|
||
// resolution path: `refundParty(int side)`. This enables the Pari_p2p
|
||
// **Flow A** (independent deposits) safely — a single-side deposit can be
|
||
// reclaimed by its owner after the deadline if the opponent never joined.
|
||
// Without this, a Flow A deposit by A with B-never-shows leaves A's funds
|
||
// permanently stuck at the escrow address (existing refund() requires a
|
||
// combined pool UTXO with poolAmount FTs).
|
||
//
|
||
// All other paths (resolveArbiter, resolveHashlock, refund) are byte-for-
|
||
// byte identical to GamePoolEscrowFlat and validate the same output
|
||
// structure. They work transparently with either:
|
||
// - one combined pool UTXO (Flow B atomic co-sign), OR
|
||
// - two separate deposit UTXOs (Flow A independent deposits)
|
||
// because the covenant only constrains outputs, not the input layout.
|
||
//
|
||
// IMPORTANT: in Flow A, EACH deposit UTXO is its own covenant instance
|
||
// spending; resolution txs (resolveArbiter / refund) spend BOTH UTXOs as
|
||
// inputs in the same tx and consensus enforces the constraints on each.
|
||
//
|
||
// Status: CHIPNET-VALIDATED by scripts/chipnet-pari-p2p-test.mjs across four
|
||
// scenarios on live chipnet consensus. Latest run (with cancel()) — CHE
|
||
// category ebd7c46a…0be36: mint+stake atomic (db444c0d…, 729578e5…),
|
||
// resolveArbiter on 2 separate deposits (a73c008e…), refundParty(0) on stuck
|
||
// single-side after deadline (ba66a060…), cancel(0, sigA, pubA, sigArbiter)
|
||
// immediate user-initiated reclaim with no deadline wait (391fdb18…). Full
|
||
// reference: Parameters/Pari_p2p/.
|
||
|
||
contract GamePoolEscrowFlatV2(
|
||
bytes tokenCategory, // the MBA being staked
|
||
int poolAmount, // total pooled FTs = 2 × stakeAmount
|
||
int poolSats, // total backing = 2 × stakeSats
|
||
int stakeAmount, // per-player stake (for refund / refundParty)
|
||
int stakeSats, // per-player backing
|
||
bytes20 partyA, // player A pkh (refund recipient)
|
||
bytes20 partyB, // player B pkh (refund recipient)
|
||
pubkey arbiterPk, // arbiter for resolveArbiter (+ fee recipient)
|
||
bytes32 secretHash, // sha256(secret) for resolveHashlock
|
||
int deadline, // CLTV height/time for refund + refundParty
|
||
int feeAmount // flat arbiter fee in FTs; 0..<poolAmount
|
||
) {
|
||
// ---- Arbiter declares the winner: pool − feeAmount → winner, feeAmount → arbiter ----
|
||
function resolveArbiter(sig arbiterSig, bytes winnerLockingBytecode) {
|
||
require(checkSig(arbiterSig, arbiterPk));
|
||
int rate = poolSats / poolAmount;
|
||
require(feeAmount < poolAmount);
|
||
int winnerAmount = poolAmount - feeAmount;
|
||
|
||
require(tx.outputs[0].lockingBytecode == winnerLockingBytecode);
|
||
require(tx.outputs[0].tokenCategory == tokenCategory);
|
||
require(tx.outputs[0].tokenAmount == winnerAmount);
|
||
require(tx.outputs[0].value == winnerAmount * rate);
|
||
|
||
if (feeAmount > 0) {
|
||
require(feeAmount * rate >= 546);
|
||
require(tx.outputs[1].lockingBytecode == new LockingBytecodeP2PKH(hash160(arbiterPk)));
|
||
require(tx.outputs[1].tokenCategory == tokenCategory);
|
||
require(tx.outputs[1].tokenAmount == feeAmount);
|
||
require(tx.outputs[1].value == feeAmount * rate);
|
||
}
|
||
}
|
||
|
||
// ---- Winner reveals the committed secret: entire pool → revealer ----
|
||
function resolveHashlock(bytes secret, bytes winnerLockingBytecode) {
|
||
require(sha256(secret) == secretHash);
|
||
require(tx.outputs[0].lockingBytecode == winnerLockingBytecode);
|
||
require(tx.outputs[0].tokenCategory == tokenCategory);
|
||
require(tx.outputs[0].tokenAmount == poolAmount);
|
||
require(tx.outputs[0].value == poolSats);
|
||
}
|
||
|
||
// ---- Combined refund: both stakes back to A and B (Flow B or paired Flow A) ----
|
||
function refund() {
|
||
require(tx.time >= deadline);
|
||
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyA));
|
||
require(tx.outputs[0].tokenCategory == tokenCategory);
|
||
require(tx.outputs[0].tokenAmount == stakeAmount);
|
||
require(tx.outputs[0].value == stakeSats);
|
||
require(tx.outputs[1].lockingBytecode == new LockingBytecodeP2PKH(partyB));
|
||
require(tx.outputs[1].tokenCategory == tokenCategory);
|
||
require(tx.outputs[1].tokenAmount == stakeAmount);
|
||
require(tx.outputs[1].value == stakeSats);
|
||
}
|
||
|
||
// ---- per-side refund (Pari_p2p Flow A safety net) ----
|
||
//
|
||
// After the deadline, a single-side deposit can be reclaimed by its
|
||
// owner. Permissionless — anyone can trigger it (they just pay the
|
||
// miner fee), but the output is structurally forced to the specified
|
||
// side's P2PKH. The input must be a SINGLE-STAKE-SIZED UTXO; this
|
||
// prevents calling refundParty on a combined pool UTXO that the
|
||
// existing refund() should handle.
|
||
//
|
||
// `side` selects which party's P2PKH receives:
|
||
// side = 0 → partyA
|
||
// side = 1 → partyB
|
||
function refundParty(int side) {
|
||
require(tx.time >= deadline);
|
||
require(side == 0 || side == 1);
|
||
|
||
// Constrain the input to a single-stake-sized deposit. This
|
||
// closes off a perverse spend where someone tries to refundParty
|
||
// a combined pool UTXO (which would otherwise pay only one side
|
||
// their stake's worth and "lose" the other side's stake to fees).
|
||
require(tx.inputs[this.activeInputIndex].tokenAmount == stakeAmount);
|
||
require(tx.inputs[this.activeInputIndex].value == stakeSats);
|
||
|
||
if (side == 0) {
|
||
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyA));
|
||
} else {
|
||
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyB));
|
||
}
|
||
require(tx.outputs[0].tokenCategory == tokenCategory);
|
||
require(tx.outputs[0].tokenAmount == stakeAmount);
|
||
require(tx.outputs[0].value == stakeSats);
|
||
}
|
||
|
||
// ---- USER-SIGNED EARLY CANCEL — both depositor and arbiter must sign ----
|
||
//
|
||
// Lets a single-side depositor reclaim BEFORE the deadline, without waiting,
|
||
// when the arbiter (game server) co-signs an attestation that the match
|
||
// was indeed cancelled / no opponent joined.
|
||
//
|
||
// Why both signatures are required:
|
||
// - depositor's sig alone → A could grief mid-game by cancelling after B
|
||
// deposited (the covenant cannot see other UTXOs at this address).
|
||
// - arbiter's sig alone → server could rug deposits unilaterally.
|
||
// - both together → arbiter attests cancellation conditions, depositor
|
||
// confirms they want their funds back. Server cooperation is required,
|
||
// and refundParty(side) after the deadline remains the no-server fallback.
|
||
//
|
||
// Caller passes `partyPk` (the depositor's compressed pubkey) so the contract
|
||
// can verify checkSig(partySig, partyPk) AND that hash160(partyPk) matches
|
||
// the right party's pkh stored at construction.
|
||
function cancel(int side, sig partySig, pubkey partyPk, sig arbiterSig) {
|
||
require(side == 0 || side == 1);
|
||
|
||
// Arbiter co-signs the cancellation attestation.
|
||
require(checkSig(arbiterSig, arbiterPk));
|
||
|
||
// Verify partyPk hashes to the right party's stored pkh, AND that this
|
||
// party signed.
|
||
if (side == 0) {
|
||
require(hash160(partyPk) == partyA);
|
||
} else {
|
||
require(hash160(partyPk) == partyB);
|
||
}
|
||
require(checkSig(partySig, partyPk));
|
||
|
||
// Same single-stake-input guard as refundParty.
|
||
require(tx.inputs[this.activeInputIndex].tokenAmount == stakeAmount);
|
||
require(tx.inputs[this.activeInputIndex].value == stakeSats);
|
||
|
||
if (side == 0) {
|
||
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyA));
|
||
} else {
|
||
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyB));
|
||
}
|
||
require(tx.outputs[0].tokenCategory == tokenCategory);
|
||
require(tx.outputs[0].tokenAmount == stakeAmount);
|
||
require(tx.outputs[0].value == stakeSats);
|
||
}
|
||
}
|