From f2dc7d5c2e782e6b7e761c674e0ff38368bfd840 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Aug 2026 09:23:07 -0700 Subject: [PATCH] first commit --- .gitignore | 7 + README.md | 112 ++ artifacts/PoolContract.json | 228 ++++ artifacts/ShareContract.json | 90 ++ config/deploy.mainnet.example.json | 23 + contracts/PoolContract.cash | 150 +++ contracts/ShareContract.cash | 54 + package-lock.json | 1661 ++++++++++++++++++++++++++++ package.json | 34 + scripts/addresses.mjs | 34 + scripts/compile.mjs | 22 + scripts/deploy.mjs | 52 + scripts/lib.mjs | 121 ++ test/helpers.js | 92 ++ test/pool-consolidate.mock.test.js | 74 ++ test/pool-migrate.mock.test.js | 76 ++ test/pool-split.mock.test.js | 133 +++ test/share-claim.mock.test.js | 74 ++ 18 files changed, 3037 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 artifacts/PoolContract.json create mode 100644 artifacts/ShareContract.json create mode 100644 config/deploy.mainnet.example.json create mode 100644 contracts/PoolContract.cash create mode 100644 contracts/ShareContract.cash create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/addresses.mjs create mode 100644 scripts/compile.mjs create mode 100644 scripts/deploy.mjs create mode 100644 scripts/lib.mjs create mode 100644 test/helpers.js create mode 100644 test/pool-consolidate.mock.test.js create mode 100644 test/pool-migrate.mock.test.js create mode 100644 test/pool-split.mock.test.js create mode 100644 test/share-claim.mock.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6809f45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +deployments/ +*.log +.DS_Store +coverage/ +.nyc_output/ +config/deploy.mainnet.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b9e9ae --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# psffpp-payments + +CashScript payout contracts for PSFFPP (Permissionless Software Foundation File Pinning Protocol) deposits on Bitcoin Cash. + +Users send BCH to a single **PoolContract** address. Once per payout epoch (blockheight-gated), the pool splits 20% to treasury and 80% into three **ShareContract** UTXOs (one per node). A K-of-M (2-of-3) operator quorum must sign before any share pays out. + +Docs: [CashScript](https://cashscript.org/) · Design: circular-economy trust-minimized federation (M=3, K=2). + +## Scope (v1) + +| In scope | Out of scope | +|----------|--------------| +| `ShareContract.cash`, `PoolContract.cash` | Cron (consolidate/split/broadcast) | +| Compile + mock tests | Timecard web GUI / sig collection | +| Address + deploy record scripts | Treasury multisig, withdrawal caps | + +## Epoch model + +Each Pool deploy bakes a single `splitBlockheight`. After that height, `split()` may run once the pool is consolidated. Later months are **not** gated by the same contract: deploy a new Pool+Shares with the next height, then K-of-M `migrate()` any remaining pool value into the new Pool. + +## Contracts + +### ShareContract + +Constructor: `node0`, `node1`, `node2` pubkeys + this node's `nodePkh`. + +- `claim(s0, s1, s2)` — requires ≥2 valid ECDSA sigs against registered pubs; pays exactly to `nodePkh`; terminating (no self-replication). Unused sigs must be empty (`0x`). + +### PoolContract + +Constructor: three pubs, `treasuryPkh`, three share **locking bytecodes**, `minConsolidation` (100_000), `splitBlockheight`. + +- `consolidate()` — permissionless; outputs must self-replicate (same P2SH locking bytecode), pure BCH, ≥ `minConsolidation`. +- `split()` — permissionless after `tx.time >= splitBlockheight`; 4 outputs: treasury (20% + rounding remainder) + three equal shares locked in the baked ShareContracts. Prefer a companion fee-paying input so the full pool value is apportioned. +- `migrate(s0, s1, s2)` — 2-of-3; destinations are authorized by the signatures (no baked next-pool bytecode). + +## Setup + +```bash +npm install +npm run compile +npm test +``` + +Requires Node.js ≥ 22. Matches `cashc` / `cashscript` `^0.13`. + +## Config + +```bash +cp config/deploy.mainnet.example.json config/deploy.mainnet.json +``` + +Fill in: + +- Three compressed operator **pubkeys** (66 hex chars) +- Three node payout **pkh** values (40 hex chars) — usually `hash160(pubkey)` for each operator’s payout key +- Treasury **pkh** +- `splitBlockheight` (absolute BCH block height for this epoch) +- `minConsolidation` (default 100000) + +`config/deploy.mainnet.json` is gitignored. Never put private keys in config. + +## Deploy order (mainnet) + +Contracts are immutable. Order matters: + +1. Freeze operator pubs / PKHs / treasury PKH / `splitBlockheight`. +2. `npm run compile` +3. `npm run addresses -- config/deploy.mainnet.json` — prints three Share addresses + Pool address. +4. `npm run deploy -- config/deploy.mainnet.json` — writes `deployments/.json` (addresses, locking bytecodes, artifact fingerprints). Does **not** move funds. +5. Publish **Pool** P2SH32 address as the deposit address. +6. **Smoke checklist** (small intentional funding only): + - Deposit a small amount to the Pool + - Run `consolidate` if multiple UTXOs + - Confirm `split` fails before the gate + - After height: `split` with a fee-paying companion input + - `claim` a share with 2-of-3 ECDSA signatures + - Optionally test `migrate` to a next-epoch Pool +7. Only then accept production deposits + +### Signing (ECDSA) + +```js +import { SignatureTemplate, HashType, SignatureAlgorithm } from 'cashscript' + +const tmpl = new SignatureTemplate( + privateKey, + HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS, + SignatureAlgorithm.ECDSA +) +``` + +Pass `Uint8Array.of()` for unused signature slots. + +### Next epoch + +1. Deploy new Shares + Pool with the next `splitBlockheight`. +2. K-of-M `migrate` remaining old-pool value to the new Pool address. +3. Claim any unclaimed old Share UTXOs separately (still valid under the old Share contracts). + +## Security notes + +- Limit outputs first in every function. +- Consolidate compares against `tx.inputs[this.activeInputIndex].lockingBytecode` (P2SH wrapper), not redeem script. +- Empty `tokenCategory` on covenant outputs (pure BCH). +- Use `tx.time >=` (never `>`). +- Fee stalls on claim/migrate require rebuilding the tx and re-collecting all K signatures. +- If ≥K operators collude they can approve any signed migrate destination — trust-minimized federation, not permissionless. + +## License + +MIT diff --git a/artifacts/PoolContract.json b/artifacts/PoolContract.json new file mode 100644 index 0000000..83e5815 --- /dev/null +++ b/artifacts/PoolContract.json @@ -0,0 +1,228 @@ +{ + "contractName": "PoolContract", + "constructorInputs": [ + { + "name": "node0", + "type": "pubkey" + }, + { + "name": "node1", + "type": "pubkey" + }, + { + "name": "node2", + "type": "pubkey" + }, + { + "name": "treasuryPkh", + "type": "bytes20" + }, + { + "name": "share0", + "type": "bytes" + }, + { + "name": "share1", + "type": "bytes" + }, + { + "name": "share2", + "type": "bytes" + }, + { + "name": "minConsolidation", + "type": "int" + }, + { + "name": "splitBlockheight", + "type": "int" + } + ], + "abi": [ + { + "name": "consolidate", + "inputs": [] + }, + { + "name": "split", + "inputs": [] + }, + { + "name": "migrate", + "inputs": [ + { + "name": "s0", + "type": "sig" + }, + { + "name": "s1", + "type": "sig" + }, + { + "name": "s2", + "type": "sig" + } + ] + } + ], + "bytecode": "OP_9 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_TXOUTPUTCOUNT OP_2 OP_LESSTHANOREQUAL OP_VERIFY OP_TXOUTPUTCOUNT OP_1 OP_GREATERTHANOREQUAL OP_VERIFY OP_0 OP_0 OP_BEGIN OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_2DUP OP_UTXOVALUE OP_ADD OP_ROT OP_DROP OP_SWAP OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_INPUTINDEX OP_UTXOBYTECODE OP_0 OP_0 OP_BEGIN OP_DUP OP_TXOUTPUTCOUNT OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_DUP OP_OUTPUTBYTECODE OP_3 OP_PICK OP_EQUALVERIFY OP_DUP OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_DUP OP_OUTPUTVALUE OP_12 OP_PICK OP_GREATERTHANOREQUAL OP_VERIFY OP_2DUP OP_OUTPUTVALUE OP_ADD OP_ROT OP_DROP OP_SWAP OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_ROT OP_LESSTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_9 OP_PICK OP_1 OP_NUMEQUAL OP_IF OP_TXOUTPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_8 OP_ROLL OP_CHECKLOCKTIMEVERIFY OP_DROP OP_TXINPUTCOUNT OP_2 OP_LESSTHANOREQUAL OP_VERIFY OP_INPUTINDEX OP_UTXOVALUE OP_DUP 14 OP_MUL 64 OP_DIV OP_2DUP OP_SUB OP_DUP OP_3 OP_DIV OP_SWAP OP_OVER OP_3 OP_MUL OP_SUB OP_ROT OP_ADD OP_0 OP_OUTPUTBYTECODE 76a914 OP_8 OP_ROLL OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_OVER OP_NUMEQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_7 OP_ROLL OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_2 OP_PICK OP_NUMEQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_7 OP_ROLL OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_2 OP_PICK OP_NUMEQUALVERIFY OP_3 OP_OUTPUTBYTECODE OP_7 OP_ROLL OP_EQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_3 OP_OUTPUTVALUE OP_2 OP_PICK OP_NUMEQUALVERIFY OP_OVER OP_ADD OP_OVER OP_ADD OP_ADD OP_NUMEQUALVERIFY OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_9 OP_ROLL OP_2 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_2 OP_LESSTHANOREQUAL OP_VERIFY OP_TXOUTPUTCOUNT OP_1 OP_GREATERTHANOREQUAL OP_VERIFY OP_0 OP_10 OP_ROLL OP_ROT OP_CHECKSIG OP_IF OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_9 OP_ROLL OP_ROT OP_CHECKSIG OP_IF OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_8 OP_ROLL OP_ROT OP_CHECKSIG OP_IF OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_2 OP_GREATERTHANOREQUAL OP_VERIFY OP_0 OP_0 OP_BEGIN OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_2DUP OP_UTXOVALUE OP_ADD OP_ROT OP_DROP OP_SWAP OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_0 OP_0 OP_BEGIN OP_DUP OP_TXOUTPUTCOUNT OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_DUP OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_DUP OP_OUTPUTVALUE OP_0 OP_GREATERTHAN OP_VERIFY OP_2DUP OP_OUTPUTVALUE OP_ADD OP_ROT OP_DROP OP_SWAP OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ENDIF OP_ENDIF", + "source": "pragma cashscript ^0.13.0;\n\n////////////////////////////////////////////////////////////////////////////////\n// PoolContract — single pool instance per payout epoch (M=3, K=2)\n//\n// Holds user deposits. Each deploy is one height-gated payout epoch.\n// Next month: redeploy with a new splitBlockheight, then K-of-M migrate.\n//\n////////////////////////////////////////////////////////////////////////////////\n\ncontract PoolContract(\n pubkey node0,\n pubkey node1,\n pubkey node2,\n bytes20 treasuryPkh,\n bytes share0,\n bytes share1,\n bytes share2,\n int minConsolidation,\n int splitBlockheight\n) {\n //////////////////////////////////////////////////////////////////////////////\n // Operation: consolidate (permissionless)\n // inputs:\n // 0..N PoolContract [BCH] (one or more pool UTXOs)\n // ? feePayer [BCH] (optional)\n // outputs:\n // 0..1 PoolContract [BCH] (self-replicating, each >= minConsolidation)\n //////////////////////////////////////////////////////////////////////////////\n function consolidate() {\n require(tx.outputs.length <= 2, \"consolidate: too many outputs\");\n require(tx.outputs.length >= 1, \"consolidate: need at least 1 output\");\n\n int inSum = 0;\n for (int i = 0; i < tx.inputs.length; i = i + 1) {\n inSum = inSum + tx.inputs[i].value;\n }\n\n // Compare against the P2SH locking bytecode on this input (not redeem script).\n bytes contractLock = tx.inputs[this.activeInputIndex].lockingBytecode;\n\n int outSum = 0;\n for (int j = 0; j < tx.outputs.length; j = j + 1) {\n require(\n tx.outputs[j].lockingBytecode == contractLock,\n \"consolidate: must self-replicate\"\n );\n require(tx.outputs[j].tokenCategory == 0x, \"consolidate: pure BCH only\");\n require(\n tx.outputs[j].value >= minConsolidation,\n \"consolidate: output below minConsolidation\"\n );\n outSum = outSum + tx.outputs[j].value;\n }\n\n require(outSum <= inSum, \"consolidate: value not conserved\");\n }\n\n //////////////////////////////////////////////////////////////////////////////\n // Operation: split (permissionless, blockheight-gated)\n // inputs:\n // 0 PoolContract [BCH] (consolidated pool UTXO — active input)\n // 1? feePayer [BCH] (optional companion fee input)\n // outputs:\n // 0 treasury [BCH] (20% + rounding remainder)\n // 1 ShareContract node0 [BCH] (equal share of 80%)\n // 2 ShareContract node1 [BCH]\n // 3 ShareContract node2 [BCH]\n //////////////////////////////////////////////////////////////////////////////\n function split() {\n require(tx.outputs.length == 4, \"split: expected 4 outputs\");\n require(tx.time >= splitBlockheight, \"split: blockheight gate not met\");\n\n // Fee companion allowed; pool value taken from this active input only.\n require(tx.inputs.length <= 2, \"split: at most 2 inputs\");\n\n int poolValue = tx.inputs[this.activeInputIndex].value;\n int treasuryBase = poolValue * 20 / 100;\n int remaining = poolValue - treasuryBase;\n int share = remaining / 3;\n int remainder = remaining - share * 3;\n int treasuryValue = treasuryBase + remainder;\n\n // Output 0: treasury\n require(\n tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(treasuryPkh),\n \"split: output 0 must be treasury\"\n );\n require(tx.outputs[0].tokenCategory == 0x, \"split: treasury must be pure BCH\");\n require(tx.outputs[0].value == treasuryValue, \"split: bad treasury value\");\n\n // Outputs 1..3: equal shares locked in ShareContracts\n require(tx.outputs[1].lockingBytecode == share0, \"split: output 1 must be share0\");\n require(tx.outputs[1].tokenCategory == 0x, \"split: share0 must be pure BCH\");\n require(tx.outputs[1].value == share, \"split: bad share0 value\");\n\n require(tx.outputs[2].lockingBytecode == share1, \"split: output 2 must be share1\");\n require(tx.outputs[2].tokenCategory == 0x, \"split: share1 must be pure BCH\");\n require(tx.outputs[2].value == share, \"split: bad share1 value\");\n\n require(tx.outputs[3].lockingBytecode == share2, \"split: output 3 must be share2\");\n require(tx.outputs[3].tokenCategory == 0x, \"split: share2 must be pure BCH\");\n require(tx.outputs[3].value == share, \"split: bad share2 value\");\n\n // Exact poolValue apportionment (fee paid by companion input if present)\n require(\n treasuryValue + share + share + share == poolValue,\n \"split: apportionment mismatch\"\n );\n }\n\n //////////////////////////////////////////////////////////////////////////////\n // Operation: migrate (K-of-M)\n // inputs:\n // 0..N PoolContract [BCH]\n // ? feePayer [BCH] (optional)\n // outputs:\n // 0..1 next-epoch destination [BCH] (authorized by signatures)\n //////////////////////////////////////////////////////////////////////////////\n function migrate(sig s0, sig s1, sig s2) {\n require(tx.outputs.length <= 2, \"migrate: too many outputs\");\n require(tx.outputs.length >= 1, \"migrate: need at least 1 output\");\n\n int validCount = 0;\n if (checkSig(s0, node0)) {\n validCount = validCount + 1;\n }\n if (checkSig(s1, node1)) {\n validCount = validCount + 1;\n }\n if (checkSig(s2, node2)) {\n validCount = validCount + 1;\n }\n require(validCount >= 2, \"migrate: need at least 2-of-3 signatures\");\n\n int inSum = 0;\n for (int i = 0; i < tx.inputs.length; i = i + 1) {\n inSum = inSum + tx.inputs[i].value;\n }\n\n int outSum = 0;\n for (int j = 0; j < tx.outputs.length; j = j + 1) {\n require(tx.outputs[j].tokenCategory == 0x, \"migrate: pure BCH only\");\n require(tx.outputs[j].value > 0, \"migrate: zero output\");\n outSum = outSum + tx.outputs[j].value;\n }\n\n require(outSum <= inSum, \"migrate: value not conserved\");\n }\n}\n", + "fingerprint": "08188769a6af9aaba3fa5e3ce3c0c32f1e4525f06373e3b78a95551db9a66d9f", + "debug": { + "bytecode": "5979009c63c452a169c451a26900006576c39f766b636ec6937b757c768b77686c916675c0c700006576c49f766b6376cd53798876d1008876cc5c79a2696ecc937b757c768b77686c9166757ba1696d6d6d6d6d7551675979519c63c4549d587ab175c352a169c0c6760114950164966e947653967c785395947b9300cd0376a914587a7e0288ac7e8800d1008800cc789d51cd577a8851d1008851cc52799d52cd577a8852d1008852cc52799d53cd577a8853d1008853cc52799d78937893939d6d6d755167597a529dc452a169c451a269005a7a7bac63768b7768597a7bac63768b7768587a7bac63768b776852a26900006576c39f766b636ec6937b757c768b77686c91667500006576c49f766b6376d1008876cc00a0696ecc937b757c768b77686c916675a2696d6d6d516868", + "sourceMap": "30:4:57:5;;;;;31:16:31:33;:37::38;:16:::1;:8::73;32:16:32:33:0;:37::38;:16:::1;:8::79;34:20:34:21:0;35:21:35:22;:8:37:9;:24:35:25;:28::44;:24:::1;;;:57:37:9:0;36:20:36:39;:28::46:1;:20;:12::47;;;35:50:35:51:0;:::55:1;:46;:57:37:9;;:8;;;40:39:40:60:0;:29::77:1;42:21:42:22:0;43::43;:8:54:9;:24:43:25;:28::45;:24:::1;;;:58:54:9:0;45:27:45:28;:16::45:1;:49::61:0;;44:12:47:14:1;48:31:48:32:0;:20::47:1;:51::53:0;:12::85:1;50:27:50:28:0;:16::35:1;:39::55:0;;:16:::1;49:12:52:14;53:21:53:42:0;:30::49:1;:21;:12::50;;;43:51:43:52:0;:::56:1;:47;:58:54:9;;:8;;;56:26:56:31:0;:16:::1;:8::69;30:27:57:5;;;;;;;:4;70::110::0;;;;;71:16:71:33;:37::38;:8::69:1;72:27:72:43:0;;:8::80:1;;75:16:75:32:0;:36::37;:16:::1;:8::66;77:34:77:55:0;:24::62:1;78:27:78:36:0;:39::41;:27:::1;:44::47:0;:27:::1;79:24:79:48:0;::::1;80:20:80:29:0;:32::33;:20:::1;81:24:81::0;:36::41;:44::45;:36:::1;:24;82:28:82:40:0;:::52:1;86:23:86:24:0;:12::41:1;:45::82:0;:70::81;;:45::82:1;;;85:8:88:10;89:27:89:28:0;:16::43:1;:47::49:0;:8::87:1;90:27:90:28:0;:16::35:1;:39::52:0;:8::83:1;93:27:93:28:0;:16::45:1;:49::55:0;;:8::91:1;94:27:94:28:0;:16::43:1;:47::49:0;:8::85:1;95:27:95:28:0;:16::35:1;:39::44:0;;:8::73:1;97:27:97:28:0;:16::45:1;:49::55:0;;:8::91:1;98:27:98:28:0;:16::43:1;:47::49:0;:8::85:1;99:27:99:28:0;:16::35:1;:39::44:0;;:8::73:1;101:27:101:28:0;:16::45:1;:49::55:0;;:8::91:1;102:27:102:28:0;:16::43:1;:47::49:0;:8::85:1;103:27:103:28:0;:16::35:1;:39::44:0;;:8::73:1;107:28:107:33:0;:12:::1;:36::41:0;:12:::1;:::49;106:8:109:10;70:21:110:5;;;;:4;120::149::0;;;;121:16:121:33;:37::38;:16:::1;:8::69;122:16:122:33:0;:37::38;:16:::1;:8::75;124:25:124:26:0;125:21:125:23;;:25::30;:12::31:1;:33:127:9:0;126:25:126:35;:::39:1;:12::40;125:33:127:9;128:21:128:23:0;;:25::30;:12::31:1;:33:130:9:0;129:25:129:35;:::39:1;:12::40;128:33:130:9;131:21:131:23:0;;:25::30;:12::31:1;:33:133:9:0;132:25:132:35;:::39:1;:12::40;131:33:133:9;134:30:134:31:0;:16:::1;:8::77;136:20:136:21:0;137:21:137:22;:8:139:9;:24:137:25;:28::44;:24:::1;;;:57:139:9:0;138:20:138:39;:28::46:1;:20;:12::47;;;137:50:137:51:0;:::55:1;:46;:57:139:9;;:8;;;141:21:141:22:0;142::142;:8:146:9;:24:142:25;:28::45;:24:::1;;;:58:146:9:0;143:31:143:32;:20::47:1;:51::53:0;:12::81:1;144:31:144:32:0;:20::39:1;:42::43:0;:20:::1;:12::69;145:21:145:42:0;:30::49:1;:21;:12::50;;;142:51:142:52:0;:::56:1;:47;:58:146:9;;:8;;;148:16:148:31;:8::65;120:45:149:5;;;;11:0:150:1;", + "logs": [], + "requires": [ + { + "ip": 17, + "line": 31, + "message": "consolidate: too many outputs" + }, + { + "ip": 21, + "line": 32, + "message": "consolidate: need at least 1 output" + }, + { + "ip": 60, + "line": 44, + "message": "consolidate: must self-replicate" + }, + { + "ip": 64, + "line": 48, + "message": "consolidate: pure BCH only" + }, + { + "ip": 70, + "line": 49, + "message": "consolidate: output below minConsolidation" + }, + { + "ip": 87, + "line": 56, + "message": "consolidate: value not conserved" + }, + { + "ip": 103, + "line": 71, + "message": "split: expected 4 outputs" + }, + { + "ip": 106, + "line": 72, + "message": "split: blockheight gate not met" + }, + { + "ip": 111, + "line": 75, + "message": "split: at most 2 inputs" + }, + { + "ip": 139, + "line": 85, + "message": "split: output 0 must be treasury" + }, + { + "ip": 143, + "line": 89, + "message": "split: treasury must be pure BCH" + }, + { + "ip": 147, + "line": 90, + "message": "split: bad treasury value" + }, + { + "ip": 152, + "line": 93, + "message": "split: output 1 must be share0" + }, + { + "ip": 156, + "line": 94, + "message": "split: share0 must be pure BCH" + }, + { + "ip": 161, + "line": 95, + "message": "split: bad share0 value" + }, + { + "ip": 166, + "line": 97, + "message": "split: output 2 must be share1" + }, + { + "ip": 170, + "line": 98, + "message": "split: share1 must be pure BCH" + }, + { + "ip": 175, + "line": 99, + "message": "split: bad share1 value" + }, + { + "ip": 180, + "line": 101, + "message": "split: output 3 must be share2" + }, + { + "ip": 184, + "line": 102, + "message": "split: share2 must be pure BCH" + }, + { + "ip": 189, + "line": 103, + "message": "split: bad share2 value" + }, + { + "ip": 195, + "line": 106, + "message": "split: apportionment mismatch" + }, + { + "ip": 208, + "line": 121, + "message": "migrate: too many outputs" + }, + { + "ip": 212, + "line": 122, + "message": "migrate: need at least 1 output" + }, + { + "ip": 243, + "line": 134, + "message": "migrate: need at least 2-of-3 signatures" + }, + { + "ip": 279, + "line": 143, + "message": "migrate: pure BCH only" + }, + { + "ip": 284, + "line": 144, + "message": "migrate: zero output" + }, + { + "ip": 300, + "line": 148, + "message": "migrate: value not conserved" + } + ], + "sourceTags": "28:30:fu;31:34:lc;35:35:sc;68:70:fu;71:74:lc;75:75:sc;250:252:fu;253:256:lc;257:257:sc;282:284:fu;285:288:lc;289:289:sc" + }, + "compiler": { + "name": "cashc", + "version": "0.13.2", + "options": { + "enforceFunctionParameterTypes": true, + "enforceLocktimeGuard": true + } + }, + "updatedAt": "2026-08-09T16:19:40.583Z" +} diff --git a/artifacts/ShareContract.json b/artifacts/ShareContract.json new file mode 100644 index 0000000..0a1e7ce --- /dev/null +++ b/artifacts/ShareContract.json @@ -0,0 +1,90 @@ +{ + "contractName": "ShareContract", + "constructorInputs": [ + { + "name": "node0", + "type": "pubkey" + }, + { + "name": "node1", + "type": "pubkey" + }, + { + "name": "node2", + "type": "pubkey" + }, + { + "name": "nodePkh", + "type": "bytes20" + } + ], + "abi": [ + { + "name": "claim", + "inputs": [ + { + "name": "s0", + "type": "sig" + }, + { + "name": "s1", + "type": "sig" + }, + { + "name": "s2", + "type": "sig" + } + ] + } + ], + "bytecode": "OP_TXOUTPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_0 OP_5 OP_ROLL OP_ROT OP_CHECKSIG OP_IF OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_4 OP_ROLL OP_ROT OP_CHECKSIG OP_IF OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_3 OP_ROLL OP_ROT OP_CHECKSIG OP_IF OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_2 OP_GREATERTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTBYTECODE 76a914 OP_ROT OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_0 OP_BEGIN OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_2DUP OP_UTXOVALUE OP_ADD OP_ROT OP_DROP OP_SWAP OP_DUP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_0 OP_OUTPUTVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTVALUE OP_0 OP_GREATERTHAN", + "source": "pragma cashscript ^0.13.0;\n\n////////////////////////////////////////////////////////////////////////////////\n// ShareContract — one instance per node (M=3, K=2)\n//\n// Holds one node's monthly share UTXO until K-of-M operators approve claim.\n//\n// Operation: claim\n// inputs:\n// 0 ShareContract [BCH] (this share UTXO)\n// 1? feePayer [BCH] (optional P2PKH fee input)\n// outputs:\n// 0 nodePayout [BCH] (P2PKH to this node's nodePkh)\n////////////////////////////////////////////////////////////////////////////////\n\ncontract ShareContract(\n pubkey node0,\n pubkey node1,\n pubkey node2,\n bytes20 nodePkh\n) {\n function claim(sig s0, sig s1, sig s2) {\n // CRITICAL: limit outputs first\n require(tx.outputs.length == 1, \"claim: expected exactly 1 output\");\n\n // K-of-M (K=2): empty sig (0x) returns false; never pass invalid non-empty sigs (NULLFAIL)\n int validCount = 0;\n if (checkSig(s0, node0)) {\n validCount = validCount + 1;\n }\n if (checkSig(s1, node1)) {\n validCount = validCount + 1;\n }\n if (checkSig(s2, node2)) {\n validCount = validCount + 1;\n }\n require(validCount >= 2, \"claim: need at least 2-of-3 signatures\");\n\n // Terminating spend to this node's payout address (no self-replication)\n require(\n tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(nodePkh),\n \"claim: output must pay nodePkh\"\n );\n require(tx.outputs[0].tokenCategory == 0x, \"claim: output must be pure BCH\");\n\n // Value conservation (fee = inputs - output)\n int inSum = 0;\n for (int i = 0; i < tx.inputs.length; i = i + 1) {\n inSum = inSum + tx.inputs[i].value;\n }\n require(tx.outputs[0].value <= inSum, \"claim: value not conserved\");\n require(tx.outputs[0].value > 0, \"claim: zero payout\");\n }\n}\n", + "fingerprint": "2937963007e829c882ed2cfd37948691c196d68c52c90a9d75f265a7b3b17a45", + "debug": { + "bytecode": "c4519d00557a7bac63768b7768547a7bac63768b7768537a7bac63768b776852a26900cd0376a9147b7e0288ac7e8800d1008800006576c39f766b636ec6937b757c768b77686c91667500cca26900cc00a0", + "sourceMap": "24:16:24:33;:37::38;:8::76:1;27:25:27:26:0;28:21:28:23;;:25::30;:12::31:1;:33:30:9:0;29:25:29:35;:::39:1;:12::40;28:33:30:9;31:21:31:23:0;;:25::30;:12::31:1;:33:33:9:0;32:25:32:35;:::39:1;:12::40;31:33:33:9;34:21:34:23:0;;:25::30;:12::31:1;:33:36:9:0;35:25:35:35;:::39:1;:12::40;34:33:36:9;37:30:37:31:0;:16:::1;:8::75;41:23:41:24:0;:12::41:1;:45::78:0;:70::77;:45::78:1;;;40:8:43:10;44:27:44:28:0;:16::43:1;:47::49:0;:8::85:1;47:20:47:21:0;48:21:48:22;:8:50:9;:24:48:25;:28::44;:24:::1;;;:57:50:9:0;49:20:49:39;:28::46:1;:20;:12::47;;;48:50:48:51:0;:::55:1;:46;:57:50:9;;:8;;;51:27:51:28:0;:16::35:1;:::44;:8::76;52:27:52:28:0;:16::35:1;:38::39:0;:8::63:1", + "logs": [], + "requires": [ + { + "ip": 6, + "line": 24, + "message": "claim: expected exactly 1 output" + }, + { + "ip": 37, + "line": 37, + "message": "claim: need at least 2-of-3 signatures" + }, + { + "ip": 45, + "line": 40, + "message": "claim: output must pay nodePkh" + }, + { + "ip": 49, + "line": 44, + "message": "claim: output must be pure BCH" + }, + { + "ip": 76, + "line": 51, + "message": "claim: value not conserved" + }, + { + "ip": 81, + "line": 52, + "message": "claim: zero payout" + } + ], + "sourceTags": "61:63:fu;64:67:lc;68:68:sc" + }, + "compiler": { + "name": "cashc", + "version": "0.13.2", + "options": { + "enforceFunctionParameterTypes": true, + "enforceLocktimeGuard": true + } + }, + "updatedAt": "2026-08-09T16:19:40.446Z" +} diff --git a/config/deploy.mainnet.example.json b/config/deploy.mainnet.example.json new file mode 100644 index 0000000..9cdded0 --- /dev/null +++ b/config/deploy.mainnet.example.json @@ -0,0 +1,23 @@ +{ + "network": "mainnet", + "minConsolidation": 100000, + "splitBlockheight": 920000, + "treasuryPkh": "0000000000000000000000000000000000000000", + "nodes": [ + { + "name": "node0", + "pubkey": "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "pkh": "1111111111111111111111111111111111111111" + }, + { + "name": "node1", + "pubkey": "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "pkh": "2222222222222222222222222222222222222222" + }, + { + "name": "node2", + "pubkey": "02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "pkh": "3333333333333333333333333333333333333333" + } + ] +} diff --git a/contracts/PoolContract.cash b/contracts/PoolContract.cash new file mode 100644 index 0000000..22e0334 --- /dev/null +++ b/contracts/PoolContract.cash @@ -0,0 +1,150 @@ +pragma cashscript ^0.13.0; + +//////////////////////////////////////////////////////////////////////////////// +// PoolContract — single pool instance per payout epoch (M=3, K=2) +// +// Holds user deposits. Each deploy is one height-gated payout epoch. +// Next month: redeploy with a new splitBlockheight, then K-of-M migrate. +// +//////////////////////////////////////////////////////////////////////////////// + +contract PoolContract( + pubkey node0, + pubkey node1, + pubkey node2, + bytes20 treasuryPkh, + bytes share0, + bytes share1, + bytes share2, + int minConsolidation, + int splitBlockheight +) { + ////////////////////////////////////////////////////////////////////////////// + // Operation: consolidate (permissionless) + // inputs: + // 0..N PoolContract [BCH] (one or more pool UTXOs) + // ? feePayer [BCH] (optional) + // outputs: + // 0..1 PoolContract [BCH] (self-replicating, each >= minConsolidation) + ////////////////////////////////////////////////////////////////////////////// + function consolidate() { + require(tx.outputs.length <= 2, "consolidate: too many outputs"); + require(tx.outputs.length >= 1, "consolidate: need at least 1 output"); + + int inSum = 0; + for (int i = 0; i < tx.inputs.length; i = i + 1) { + inSum = inSum + tx.inputs[i].value; + } + + // Compare against the P2SH locking bytecode on this input (not redeem script). + bytes contractLock = tx.inputs[this.activeInputIndex].lockingBytecode; + + int outSum = 0; + for (int j = 0; j < tx.outputs.length; j = j + 1) { + require( + tx.outputs[j].lockingBytecode == contractLock, + "consolidate: must self-replicate" + ); + require(tx.outputs[j].tokenCategory == 0x, "consolidate: pure BCH only"); + require( + tx.outputs[j].value >= minConsolidation, + "consolidate: output below minConsolidation" + ); + outSum = outSum + tx.outputs[j].value; + } + + require(outSum <= inSum, "consolidate: value not conserved"); + } + + ////////////////////////////////////////////////////////////////////////////// + // Operation: split (permissionless, blockheight-gated) + // inputs: + // 0 PoolContract [BCH] (consolidated pool UTXO — active input) + // 1? feePayer [BCH] (optional companion fee input) + // outputs: + // 0 treasury [BCH] (20% + rounding remainder) + // 1 ShareContract node0 [BCH] (equal share of 80%) + // 2 ShareContract node1 [BCH] + // 3 ShareContract node2 [BCH] + ////////////////////////////////////////////////////////////////////////////// + function split() { + require(tx.outputs.length == 4, "split: expected 4 outputs"); + require(tx.time >= splitBlockheight, "split: blockheight gate not met"); + + // Fee companion allowed; pool value taken from this active input only. + require(tx.inputs.length <= 2, "split: at most 2 inputs"); + + int poolValue = tx.inputs[this.activeInputIndex].value; + int treasuryBase = poolValue * 20 / 100; + int remaining = poolValue - treasuryBase; + int share = remaining / 3; + int remainder = remaining - share * 3; + int treasuryValue = treasuryBase + remainder; + + // Output 0: treasury + require( + tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(treasuryPkh), + "split: output 0 must be treasury" + ); + require(tx.outputs[0].tokenCategory == 0x, "split: treasury must be pure BCH"); + require(tx.outputs[0].value == treasuryValue, "split: bad treasury value"); + + // Outputs 1..3: equal shares locked in ShareContracts + require(tx.outputs[1].lockingBytecode == share0, "split: output 1 must be share0"); + require(tx.outputs[1].tokenCategory == 0x, "split: share0 must be pure BCH"); + require(tx.outputs[1].value == share, "split: bad share0 value"); + + require(tx.outputs[2].lockingBytecode == share1, "split: output 2 must be share1"); + require(tx.outputs[2].tokenCategory == 0x, "split: share1 must be pure BCH"); + require(tx.outputs[2].value == share, "split: bad share1 value"); + + require(tx.outputs[3].lockingBytecode == share2, "split: output 3 must be share2"); + require(tx.outputs[3].tokenCategory == 0x, "split: share2 must be pure BCH"); + require(tx.outputs[3].value == share, "split: bad share2 value"); + + // Exact poolValue apportionment (fee paid by companion input if present) + require( + treasuryValue + share + share + share == poolValue, + "split: apportionment mismatch" + ); + } + + ////////////////////////////////////////////////////////////////////////////// + // Operation: migrate (K-of-M) + // inputs: + // 0..N PoolContract [BCH] + // ? feePayer [BCH] (optional) + // outputs: + // 0..1 next-epoch destination [BCH] (authorized by signatures) + ////////////////////////////////////////////////////////////////////////////// + function migrate(sig s0, sig s1, sig s2) { + require(tx.outputs.length <= 2, "migrate: too many outputs"); + require(tx.outputs.length >= 1, "migrate: need at least 1 output"); + + int validCount = 0; + if (checkSig(s0, node0)) { + validCount = validCount + 1; + } + if (checkSig(s1, node1)) { + validCount = validCount + 1; + } + if (checkSig(s2, node2)) { + validCount = validCount + 1; + } + require(validCount >= 2, "migrate: need at least 2-of-3 signatures"); + + int inSum = 0; + for (int i = 0; i < tx.inputs.length; i = i + 1) { + inSum = inSum + tx.inputs[i].value; + } + + int outSum = 0; + for (int j = 0; j < tx.outputs.length; j = j + 1) { + require(tx.outputs[j].tokenCategory == 0x, "migrate: pure BCH only"); + require(tx.outputs[j].value > 0, "migrate: zero output"); + outSum = outSum + tx.outputs[j].value; + } + + require(outSum <= inSum, "migrate: value not conserved"); + } +} diff --git a/contracts/ShareContract.cash b/contracts/ShareContract.cash new file mode 100644 index 0000000..378ae41 --- /dev/null +++ b/contracts/ShareContract.cash @@ -0,0 +1,54 @@ +pragma cashscript ^0.13.0; + +//////////////////////////////////////////////////////////////////////////////// +// ShareContract — one instance per node (M=3, K=2) +// +// Holds one node's monthly share UTXO until K-of-M operators approve claim. +// +// Operation: claim +// inputs: +// 0 ShareContract [BCH] (this share UTXO) +// 1? feePayer [BCH] (optional P2PKH fee input) +// outputs: +// 0 nodePayout [BCH] (P2PKH to this node's nodePkh) +//////////////////////////////////////////////////////////////////////////////// + +contract ShareContract( + pubkey node0, + pubkey node1, + pubkey node2, + bytes20 nodePkh +) { + function claim(sig s0, sig s1, sig s2) { + // CRITICAL: limit outputs first + require(tx.outputs.length == 1, "claim: expected exactly 1 output"); + + // K-of-M (K=2): empty sig (0x) returns false; never pass invalid non-empty sigs (NULLFAIL) + int validCount = 0; + if (checkSig(s0, node0)) { + validCount = validCount + 1; + } + if (checkSig(s1, node1)) { + validCount = validCount + 1; + } + if (checkSig(s2, node2)) { + validCount = validCount + 1; + } + require(validCount >= 2, "claim: need at least 2-of-3 signatures"); + + // Terminating spend to this node's payout address (no self-replication) + require( + tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(nodePkh), + "claim: output must pay nodePkh" + ); + require(tx.outputs[0].tokenCategory == 0x, "claim: output must be pure BCH"); + + // Value conservation (fee = inputs - output) + int inSum = 0; + for (int i = 0; i < tx.inputs.length; i = i + 1) { + inSum = inSum + tx.inputs[i].value; + } + require(tx.outputs[0].value <= inSum, "claim: value not conserved"); + require(tx.outputs[0].value > 0, "claim: zero payout"); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b8b260c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1661 @@ +{ + "name": "psffpp-payments", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "psffpp-payments", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@bitauth/libauth": "^3.0.0", + "cashc": "^0.13.2", + "cashscript": "^0.13.2" + }, + "devDependencies": { + "c8": "^10.1.3", + "mocha": "^11.7.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bitauth/libauth": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.0.0.tgz", + "integrity": "sha512-3yoL31XpnhAnf5nDVMFk4xPqebxDwXrgYAwpa31ARJnV5A/eXWlpNYvCd6FTZPFM4VvKfjCBi+jRCrw1hOZ0Jg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@cashscript/utils": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@cashscript/utils/-/utils-0.13.2.tgz", + "integrity": "sha512-8xryhzZcQs2wPiiDbvvMBhc6RB3eeJW4BJIxlRVRnr4P/9gbUe4EqS+xhHe5rVX/cj4EIDUOHBzYNZJwvsak5g==", + "license": "MIT", + "dependencies": { + "@bitauth/libauth": "^3.1.0-next.8" + } + }, + "node_modules/@cashscript/utils/node_modules/@bitauth/libauth": { + "version": "3.1.0-next.8", + "resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz", + "integrity": "sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@electrum-cash/debug-logs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@electrum-cash/debug-logs/-/debug-logs-1.0.0.tgz", + "integrity": "sha512-GU/CvRR9lZ0d8gy9CXGW7f//OHCIydBavv9q+JcxjGj8Xr7HwlGqHx+Wzhx9y3YmJrXfExpgClcd++gTjdEmzA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + } + }, + "node_modules/@electrum-cash/network": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@electrum-cash/network/-/network-4.2.2.tgz", + "integrity": "sha512-v2Wwt2o0VBBALPArx2dJEDvSqewKjiTW5KAd+jEXxxgdSxFygjJIrFcXryKOCv2CDbpE5+lXAogPAjx6FqW/nw==", + "license": "MIT", + "dependencies": { + "@electrum-cash/debug-logs": "^1.0.0", + "@electrum-cash/web-socket": "^1.2.3", + "async-mutex": "^0.5.0", + "debug": "^4.3.2", + "eventemitter3": "^5.0.1", + "lossless-json": "^4.0.1" + } + }, + "node_modules/@electrum-cash/web-socket": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@electrum-cash/web-socket/-/web-socket-1.2.3.tgz", + "integrity": "sha512-sFWujTt98mvsMvE6gWjG44evkF3PcZhG1JffkkEUAVan+c9X51wkiWr3hkjPeIW0WfNpMTrFkvpGqVYmRj1RCA==", + "license": "MIT", + "dependencies": { + "@electrum-cash/debug-logs": "^1.0.0", + "@monsterbitar/isomorphic-ws": "^5.3.0", + "@types/ws": "^8.5.5", + "async-mutex": "^0.5.0", + "eventemitter3": "^5.0.1", + "lossless-json": "^4.0.1", + "ws": "^8.13.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monsterbitar/isomorphic-ws": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@monsterbitar/isomorphic-ws/-/isomorphic-ws-5.3.1.tgz", + "integrity": "sha512-BWfWUffbg3uO4K6Cyokg9ff43lPaXAOZcCnNe1lcjCjUMDVrRAb5qEHG5qeJp3ud2SPYbORaNsls5as6SR3oig==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antlr4": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz", + "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cashc": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/cashc/-/cashc-0.13.2.tgz", + "integrity": "sha512-wNh7M+ZSYumREICF0bPqGkAXBSQErnxwZY+vL8V+b58c0XqQ3YSMW7NStsM0veCyn1P18k7eby8gNitQJI5N/Q==", + "license": "MIT", + "dependencies": { + "@bitauth/libauth": "^3.1.0-next.8", + "@cashscript/utils": "^0.13.2", + "antlr4": "^4.13.2", + "commander": "^14.0.0", + "semver": "^7.7.2" + }, + "bin": { + "cashc": "dist/cashc-cli.js" + } + }, + "node_modules/cashc/node_modules/@bitauth/libauth": { + "version": "3.1.0-next.8", + "resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz", + "integrity": "sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/cashscript": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/cashscript/-/cashscript-0.13.2.tgz", + "integrity": "sha512-sFIuFq5K68Iz9I8T31ZVxgo/ybzAHahlLhKUoMEsWNiapyxkysbfKigJX1RW2p/Ksp9SSmcyG22MwPTvsz1xnA==", + "license": "MIT", + "dependencies": { + "@bitauth/libauth": "^3.1.0-next.8", + "@cashscript/utils": "^0.13.2", + "@electrum-cash/network": "^4.1.3", + "fflate": "^0.8.2", + "semver": "^7.7.2" + } + }, + "node_modules/cashscript/node_modules/@bitauth/libauth": { + "version": "3.1.0-next.8", + "resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz", + "integrity": "sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lossless-json": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.1.tgz", + "integrity": "sha512-SqD/Bg3ZfltBJ2Z14hJ/BihnvtV553WO4g9/ePtlp4lrnl9jF3AdIJt53A/Wkg/0Li+LMfxaBqgx1MiFZdQlpQ==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e7ad0e3 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "psffpp-payments", + "version": "1.0.0", + "description": "CashScript payout contracts for PSFFPP circular-economy payments on Bitcoin Cash", + "type": "module", + "main": "index.js", + "scripts": { + "compile": "node scripts/compile.mjs", + "addresses": "node scripts/addresses.mjs", + "deploy": "node scripts/deploy.mjs", + "test": "npm run compile && mocha --timeout 30000 'test/**/*.test.js'", + "test:unit": "npm test" + }, + "keywords": [ + "bitcoin-cash", + "cashscript", + "psffpp", + "smart-contracts" + ], + "author": "Permissionless Software Foundation", + "license": "MIT", + "dependencies": { + "@bitauth/libauth": "^3.0.0", + "cashc": "^0.13.2", + "cashscript": "^0.13.2" + }, + "devDependencies": { + "c8": "^10.1.3", + "mocha": "^11.7.1" + }, + "engines": { + "node": ">=22" + } +} diff --git a/scripts/addresses.mjs b/scripts/addresses.mjs new file mode 100644 index 0000000..388320e --- /dev/null +++ b/scripts/addresses.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/** + * Print deterministic P2SH32 addresses from a deploy config (read-only). + * + * Usage: + * node scripts/addresses.mjs [config/deploy.mainnet.json] + */ +import { + loadDeployConfig, + createProvider, + instantiateContracts, + buildDeploymentRecord, + root +} from './lib.mjs' +import { join } from 'node:path' + +const configPath = process.argv[2] || join(root, 'config', 'deploy.mainnet.json') +const cfg = loadDeployConfig(configPath) +const provider = createProvider(cfg.network || 'mainnet') +const inst = instantiateContracts(cfg, provider) +const record = buildDeploymentRecord(cfg, inst) + +console.log(JSON.stringify({ + network: record.network, + poolAddress: record.poolAddress, + shares: record.nodes.map((n) => ({ + name: n.name, + shareAddress: n.shareAddress, + pkh: n.pkh + })), + splitBlockheight: record.splitBlockheight, + minConsolidation: record.minConsolidation, + artifacts: record.artifacts +}, null, 2)) diff --git a/scripts/compile.mjs b/scripts/compile.mjs new file mode 100644 index 0000000..f128e0f --- /dev/null +++ b/scripts/compile.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env node +import { mkdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { compileFile } from 'cashc' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const contractsDir = join(root, 'contracts') +const artifactsDir = join(root, 'artifacts') + +mkdirSync(artifactsDir, { recursive: true }) + +const files = ['ShareContract.cash', 'PoolContract.cash'] + +for (const file of files) { + const src = join(contractsDir, file) + const artifact = compileFile(src) + const outName = file.replace(/\.cash$/, '.json') + const outPath = join(artifactsDir, outName) + writeFileSync(outPath, JSON.stringify(artifact, null, 2) + '\n') + console.log(`compiled ${file} -> artifacts/${outName}`) +} diff --git a/scripts/deploy.mjs b/scripts/deploy.mjs new file mode 100644 index 0000000..49d5081 --- /dev/null +++ b/scripts/deploy.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +/** + * Instantiate contracts against the configured network and write a deployment record. + * Does not broadcast or move funds — funding is a normal send to poolAddress. + * + * Usage: + * node scripts/deploy.mjs [config/deploy.mainnet.json] + */ +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { + loadDeployConfig, + createProvider, + instantiateContracts, + buildDeploymentRecord, + root +} from './lib.mjs' + +const configPath = process.argv[2] || join(root, 'config', 'deploy.mainnet.json') +const cfg = loadDeployConfig(configPath) + +if ((cfg.network || 'mainnet') === 'mainnet') { + const placeholder = /^0{40}$|^1{40}$|^2{40}$|^3{40}$/i + if (placeholder.test(cfg.treasuryPkh) || cfg.nodes.some((n) => placeholder.test(n.pkh) || /^(02a+|02b+|02c+)/i.test(n.pubkey))) { + console.warn('WARNING: config still looks like example placeholders. Replace with real keys before funding.') + } +} + +const provider = createProvider(cfg.network || 'mainnet') +const inst = instantiateContracts(cfg, provider) +const record = buildDeploymentRecord(cfg, inst) + +const outDir = join(root, 'deployments') +mkdirSync(outDir, { recursive: true }) +const stamp = record.createdAt.replace(/[:.]/g, '-') +const outPath = join(outDir, `${stamp}.json`) +writeFileSync(outPath, JSON.stringify(record, null, 2) + '\n') + +console.log(JSON.stringify({ + wrote: outPath, + poolAddress: record.poolAddress, + shareAddresses: record.nodes.map((n) => n.shareAddress), + splitBlockheight: record.splitBlockheight, + artifacts: record.artifacts +}, null, 2)) + +console.log(` +Next steps: + 1. Verify addresses match: npm run addresses -- ${configPath} + 2. Smoke-fund poolAddress with a small intentional amount + 3. Exercise consolidate / split / claim before accepting production deposits +`) diff --git a/scripts/lib.mjs b/scripts/lib.mjs new file mode 100644 index 0000000..34435bd --- /dev/null +++ b/scripts/lib.mjs @@ -0,0 +1,121 @@ +import { createHash } from 'node:crypto' +import { readFileSync, existsSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { hexToBin, binToHex } from '@bitauth/libauth' +import { Contract, ElectrumNetworkProvider, MockNetworkProvider } from 'cashscript' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') + +export function loadArtifact (name) { + return JSON.parse(readFileSync(join(root, 'artifacts', `${name}.json`), 'utf8')) +} + +export function artifactFingerprint (artifact) { + return createHash('sha256') + .update(JSON.stringify({ bytecode: artifact.bytecode, abi: artifact.abi, constructorInputs: artifact.constructorInputs })) + .digest('hex') +} + +export function loadDeployConfig (configPath) { + const path = resolve(configPath) + if (!existsSync(path)) { + throw new Error(`Config not found: ${path}. Copy config/deploy.mainnet.example.json to config/deploy.mainnet.json and fill real values.`) + } + const cfg = JSON.parse(readFileSync(path, 'utf8')) + validateConfig(cfg) + return cfg +} + +function validateConfig (cfg) { + if (!cfg.nodes || cfg.nodes.length !== 3) { + throw new Error('config.nodes must contain exactly 3 entries (M=3)') + } + for (const [i, node] of cfg.nodes.entries()) { + if (!/^[0-9a-fA-F]{66}$/.test(node.pubkey)) { + throw new Error(`nodes[${i}].pubkey must be 33-byte compressed hex (66 chars)`) + } + if (!/^[0-9a-fA-F]{40}$/.test(node.pkh)) { + throw new Error(`nodes[${i}].pkh must be 20-byte hex (40 chars)`) + } + } + if (!/^[0-9a-fA-F]{40}$/.test(cfg.treasuryPkh)) { + throw new Error('treasuryPkh must be 20-byte hex (40 chars)') + } + if (!Number.isInteger(cfg.minConsolidation) || cfg.minConsolidation < 1) { + throw new Error('minConsolidation must be a positive integer') + } + if (!Number.isInteger(cfg.splitBlockheight) || cfg.splitBlockheight < 1) { + throw new Error('splitBlockheight must be a positive integer (block height)') + } +} + +export function createProvider (network) { + if (network === 'mocknet') return new MockNetworkProvider() + return new ElectrumNetworkProvider(network || 'mainnet') +} + +export function instantiateContracts (cfg, provider) { + const shareArtifact = loadArtifact('ShareContract') + const poolArtifact = loadArtifact('PoolContract') + + const pubs = cfg.nodes.map((n) => hexToBin(n.pubkey)) + const shares = cfg.nodes.map((node) => + new Contract( + shareArtifact, + [...pubs, hexToBin(node.pkh)], + { provider } + ) + ) + + const pool = new Contract( + poolArtifact, + [ + ...pubs, + hexToBin(cfg.treasuryPkh), + hexToBin(shares[0].lockingBytecode), + hexToBin(shares[1].lockingBytecode), + hexToBin(shares[2].lockingBytecode), + BigInt(cfg.minConsolidation), + BigInt(cfg.splitBlockheight) + ], + { provider } + ) + + return { + shares, + pool, + fingerprints: { + ShareContract: artifactFingerprint(shareArtifact), + PoolContract: artifactFingerprint(poolArtifact) + } + } +} + +export function buildDeploymentRecord (cfg, { shares, pool, fingerprints }) { + return { + network: cfg.network || 'mainnet', + createdAt: new Date().toISOString(), + minConsolidation: cfg.minConsolidation, + splitBlockheight: cfg.splitBlockheight, + treasuryPkh: cfg.treasuryPkh.toLowerCase(), + nodes: cfg.nodes.map((n, i) => ({ + name: n.name || `node${i}`, + pubkey: n.pubkey.toLowerCase(), + pkh: n.pkh.toLowerCase(), + shareAddress: shares[i].address, + shareLockingBytecode: shares[i].lockingBytecode + })), + poolAddress: pool.address, + poolLockingBytecode: pool.lockingBytecode, + artifacts: fingerprints, + constructorNotes: { + M: 3, + K: 2, + deployOrder: 'ShareContracts first, then PoolContract baking share locking bytecodes', + epochModel: 'One splitBlockheight per deploy; next month redeploy + K-of-M migrate' + } + } +} + +export { root, binToHex, hexToBin } diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..46d939d --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,92 @@ +import { + Contract, + HashType, + MockNetworkProvider, + SignatureAlgorithm, + SignatureTemplate +} from 'cashscript' +import { + generatePrivateKey, + hash160, + hexToBin, + privateKeyToP2pkhCashAddress +} from '@bitauth/libauth' +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') + +export const EMPTY_SIG = Uint8Array.of() +export const SIGHASH = HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS + +export function loadArtifact (name) { + return JSON.parse(readFileSync(join(root, 'artifacts', `${name}.json`), 'utf8')) +} + +export function ecdsaTemplate (privateKey) { + return new SignatureTemplate(privateKey, SIGHASH, SignatureAlgorithm.ECDSA) +} + +export function makeOperator (label = 'node') { + const priv = generatePrivateKey(() => crypto.getRandomValues(new Uint8Array(32))) + const tmpl = ecdsaTemplate(priv) + const pub = tmpl.getPublicKey() + const pkh = hash160(pub) + const addr = privateKeyToP2pkhCashAddress({ + privateKey: priv, + prefix: 'bchtest', + throwErrors: true + }).address + return { label, priv, tmpl, pub, pkh, addr } +} + +export function makeTreasury () { + return makeOperator('treasury') +} + +export function makeProvider () { + return new MockNetworkProvider() +} + +export function createShares (provider, nodes) { + const artifact = loadArtifact('ShareContract') + return nodes.map((node) => + new Contract( + artifact, + [nodes[0].pub, nodes[1].pub, nodes[2].pub, node.pkh], + { provider } + ) + ) +} + +export function createPool (provider, { + nodes, + treasuryPkh, + shareContracts, + minConsolidation = 100_000n, + splitBlockheight = 1000n +}) { + const artifact = loadArtifact('PoolContract') + return new Contract( + artifact, + [ + nodes[0].pub, + nodes[1].pub, + nodes[2].pub, + treasuryPkh, + hexToBin(shareContracts[0].lockingBytecode), + hexToBin(shareContracts[1].lockingBytecode), + hexToBin(shareContracts[2].lockingBytecode), + minConsolidation, + splitBlockheight + ], + { provider } + ) +} + +export function claimArgs (nodes, signers) { + return nodes.map((node) => (signers.includes(node) ? node.tmpl : EMPTY_SIG)) +} + +export { hexToBin } diff --git a/test/pool-consolidate.mock.test.js b/test/pool-consolidate.mock.test.js new file mode 100644 index 0000000..0933a8f --- /dev/null +++ b/test/pool-consolidate.mock.test.js @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import { + TransactionBuilder, + randomUtxo +} from 'cashscript' +import { + createPool, + createShares, + makeOperator, + makeProvider, + makeTreasury +} from './helpers.js' + +describe('PoolContract.consolidate', function () { + let provider, nodes, treasury, shares, pool + + beforeEach(function () { + provider = makeProvider() + nodes = [makeOperator('n0'), makeOperator('n1'), makeOperator('n2')] + treasury = makeTreasury() + shares = createShares(provider, nodes) + pool = createPool(provider, { + nodes, + treasuryPkh: treasury.pkh, + shareContracts: shares, + minConsolidation: 100_000n, + splitBlockheight: 1000n + }) + }) + + it('merges multiple pool UTXOs into one self-replicating output', async function () { + const u0 = provider.addUtxo(pool.address, randomUtxo({ satoshis: 120_000n })) + const u1 = provider.addUtxo(pool.address, randomUtxo({ satoshis: 130_000n })) + const fee = 5_000n + const out = 120_000n + 130_000n - fee + + const tx = await new TransactionBuilder({ provider }) + .addInput(u0, pool.unlock.consolidate()) + .addInput(u1, pool.unlock.consolidate()) + .addOutput({ to: pool.address, amount: out }) + .send() + + assert.ok(tx.txid) + }) + + it('rejects escape to a foreign locking bytecode', async function () { + const u0 = provider.addUtxo(pool.address, randomUtxo({ satoshis: 200_000n })) + const builder = new TransactionBuilder({ provider }) + .addInput(u0, pool.unlock.consolidate()) + .addOutput({ to: nodes[0].addr, amount: 199_000n }) + + await assert.rejects(() => builder.send(), /must self-replicate|Failed to evaluate|require/i) + }) + + it('rejects outputs below minConsolidation', async function () { + const u0 = provider.addUtxo(pool.address, randomUtxo({ satoshis: 150_000n })) + const builder = new TransactionBuilder({ provider }) + .addInput(u0, pool.unlock.consolidate()) + .addOutput({ to: pool.address, amount: 50_000n }) + + await assert.rejects(() => builder.send(), /below minConsolidation|Failed to evaluate|require/i) + }) + + it('rejects more than two outputs', async function () { + const u0 = provider.addUtxo(pool.address, randomUtxo({ satoshis: 400_000n })) + const builder = new TransactionBuilder({ provider }) + .addInput(u0, pool.unlock.consolidate()) + .addOutput({ to: pool.address, amount: 130_000n }) + .addOutput({ to: pool.address, amount: 130_000n }) + .addOutput({ to: pool.address, amount: 139_000n }) + + await assert.rejects(() => builder.send(), /too many outputs|Failed to evaluate|require/i) + }) +}) diff --git a/test/pool-migrate.mock.test.js b/test/pool-migrate.mock.test.js new file mode 100644 index 0000000..6b12a5e --- /dev/null +++ b/test/pool-migrate.mock.test.js @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict' +import { + TransactionBuilder, + randomUtxo +} from 'cashscript' +import { + claimArgs, + createPool, + createShares, + makeOperator, + makeProvider, + makeTreasury +} from './helpers.js' + +describe('PoolContract.migrate', function () { + let provider, nodes, treasury, shares, pool, nextPool + + beforeEach(function () { + provider = makeProvider() + nodes = [makeOperator('n0'), makeOperator('n1'), makeOperator('n2')] + treasury = makeTreasury() + shares = createShares(provider, nodes) + pool = createPool(provider, { + nodes, + treasuryPkh: treasury.pkh, + shareContracts: shares, + minConsolidation: 100_000n, + splitBlockheight: 1000n + }) + nextPool = createPool(provider, { + nodes, + treasuryPkh: treasury.pkh, + shareContracts: shares, + minConsolidation: 100_000n, + splitBlockheight: 2000n + }) + }) + + it('allows 2-of-3 to migrate value to the next-epoch pool', async function () { + const utxo = provider.addUtxo(pool.address, randomUtxo({ satoshis: 500_000n })) + const fee = 1000n + const amount = 500_000n - fee + + const tx = await new TransactionBuilder({ provider }) + .addInput(utxo, pool.unlock.migrate(...claimArgs(nodes, [nodes[0], nodes[1]]))) + .addOutput({ to: nextPool.address, amount }) + .send() + + assert.ok(tx.txid) + }) + + it('rejects migration with only 1-of-3 signatures', async function () { + const utxo = provider.addUtxo(pool.address, randomUtxo({ satoshis: 500_000n })) + const fee = 1000n + const amount = 500_000n - fee + + const builder = new TransactionBuilder({ provider }) + .addInput(utxo, pool.unlock.migrate(...claimArgs(nodes, [nodes[0]]))) + .addOutput({ to: nextPool.address, amount }) + + await assert.rejects(() => builder.send(), /need at least 2-of-3|Failed to evaluate|require/i) + }) + + it('allows 2-of-3 to send to an arbitrary signed destination', async function () { + const utxo = provider.addUtxo(pool.address, randomUtxo({ satoshis: 250_000n })) + const fee = 1000n + const amount = 250_000n - fee + + const tx = await new TransactionBuilder({ provider }) + .addInput(utxo, pool.unlock.migrate(...claimArgs(nodes, [nodes[1], nodes[2]]))) + .addOutput({ to: nodes[0].addr, amount }) + .send() + + assert.ok(tx.txid) + }) +}) diff --git a/test/pool-split.mock.test.js b/test/pool-split.mock.test.js new file mode 100644 index 0000000..8b809cd --- /dev/null +++ b/test/pool-split.mock.test.js @@ -0,0 +1,133 @@ +import assert from 'node:assert/strict' +import { + TransactionBuilder, + randomUtxo +} from 'cashscript' +import { + createPool, + createShares, + makeOperator, + makeProvider, + makeTreasury +} from './helpers.js' + +function apportion (poolValue) { + const treasuryBase = (poolValue * 20n) / 100n + const remaining = poolValue - treasuryBase + const share = remaining / 3n + const remainder = remaining - share * 3n + return { + treasuryValue: treasuryBase + remainder, + share + } +} + +describe('PoolContract.split', function () { + const splitHeight = 1000n + let provider, nodes, treasury, shares, pool + + beforeEach(function () { + provider = makeProvider() + provider.setBlockHeight(Number(splitHeight)) + nodes = [makeOperator('n0'), makeOperator('n1'), makeOperator('n2')] + treasury = makeTreasury() + shares = createShares(provider, nodes) + pool = createPool(provider, { + nodes, + treasuryPkh: treasury.pkh, + shareContracts: shares, + minConsolidation: 100_000n, + splitBlockheight: splitHeight + }) + }) + + it('rejects split before the gated blockheight', async function () { + provider.setBlockHeight(Number(splitHeight) - 1) + const poolValue = 1_000_000n + const feePriv = makeOperator('fee') + const poolUtxo = provider.addUtxo(pool.address, randomUtxo({ satoshis: poolValue })) + const feeUtxo = provider.addUtxo(feePriv.addr, randomUtxo({ satoshis: 5_000n })) + const { treasuryValue, share } = apportion(poolValue) + + const builder = new TransactionBuilder({ provider }) + .addInput(poolUtxo, pool.unlock.split()) + .addInput(feeUtxo, feePriv.tmpl.unlockP2PKH()) + .addOutput({ to: treasury.addr, amount: treasuryValue }) + .addOutput({ to: shares[0].address, amount: share }) + .addOutput({ to: shares[1].address, amount: share }) + .addOutput({ to: shares[2].address, amount: share }) + .setLocktime(Number(splitHeight) - 1) + + await assert.rejects(() => builder.send(), /blockheight gate|Failed to evaluate|require|non-final|locktime/i) + }) + + it('splits 20% treasury + equal 80%/3 shares after the gate', async function () { + const poolValue = 1_000_000n + const feePriv = makeOperator('fee') + const poolUtxo = provider.addUtxo(pool.address, randomUtxo({ satoshis: poolValue })) + const feeUtxo = provider.addUtxo(feePriv.addr, randomUtxo({ satoshis: 5_000n })) + const { treasuryValue, share } = apportion(poolValue) + + assert.equal(treasuryValue + share * 3n, poolValue) + + const tx = await new TransactionBuilder({ provider }) + .addInput(poolUtxo, pool.unlock.split()) + .addInput(feeUtxo, feePriv.tmpl.unlockP2PKH()) + .addOutput({ to: treasury.addr, amount: treasuryValue }) + .addOutput({ to: shares[0].address, amount: share }) + .addOutput({ to: shares[1].address, amount: share }) + .addOutput({ to: shares[2].address, amount: share }) + .setLocktime(Number(splitHeight)) + .send() + + assert.ok(tx.txid) + // remaining = 800000; share = 266666; remainder = 2 → treasury = 200002 + assert.equal(share, 266_666n) + assert.equal(treasuryValue, 200_002n) + }) + + it('rejects wrong share locking bytecode destinations', async function () { + const poolValue = 1_000_000n + const feePriv = makeOperator('fee') + const poolUtxo = provider.addUtxo(pool.address, randomUtxo({ satoshis: poolValue })) + const feeUtxo = provider.addUtxo(feePriv.addr, randomUtxo({ satoshis: 5_000n })) + const { treasuryValue, share } = apportion(poolValue) + + const builder = new TransactionBuilder({ provider }) + .addInput(poolUtxo, pool.unlock.split()) + .addInput(feeUtxo, feePriv.tmpl.unlockP2PKH()) + .addOutput({ to: treasury.addr, amount: treasuryValue }) + .addOutput({ to: nodes[0].addr, amount: share }) + .addOutput({ to: shares[1].address, amount: share }) + .addOutput({ to: shares[2].address, amount: share }) + .setLocktime(Number(splitHeight)) + + await assert.rejects(() => builder.send(), /output 1 must be share0|Failed to evaluate|require/i) + }) + + it('folds integer-division remainder into the treasury', async function () { + // 100001 * 20/100 = 20000; remaining 80001; share 26667; rem 80001-80001=0? + // 80001 / 3 = 26667, rem = 80001 - 80001 = 0 + // Use 100003: remaining = 80003 - wait 100003*20/100=20000, rem=80003, share=26667, r=80003-80001=2 + const poolValue = 100_003n + const feePriv = makeOperator('fee') + const poolUtxo = provider.addUtxo(pool.address, randomUtxo({ satoshis: poolValue })) + const feeUtxo = provider.addUtxo(feePriv.addr, randomUtxo({ satoshis: 5_000n })) + const { treasuryValue, share } = apportion(poolValue) + + assert.equal(share, 26_667n) + assert.equal(treasuryValue, 20_002n) + + const tx = await new TransactionBuilder({ provider }) + .addInput(poolUtxo, pool.unlock.split()) + .addInput(feeUtxo, feePriv.tmpl.unlockP2PKH()) + .addOutput({ to: treasury.addr, amount: treasuryValue }) + .addOutput({ to: shares[0].address, amount: share }) + .addOutput({ to: shares[1].address, amount: share }) + .addOutput({ to: shares[2].address, amount: share }) + .setLocktime(Number(splitHeight)) + .send() + + assert.ok(tx.txid) + }) +}) diff --git a/test/share-claim.mock.test.js b/test/share-claim.mock.test.js new file mode 100644 index 0000000..3edafa3 --- /dev/null +++ b/test/share-claim.mock.test.js @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import { + TransactionBuilder, + randomUtxo +} from 'cashscript' +import { + claimArgs, + createShares, + makeOperator, + makeProvider +} from './helpers.js' + +describe('ShareContract.claim', function () { + let provider, nodes, shares, share0, utxo + + beforeEach(function () { + provider = makeProvider() + nodes = [makeOperator('n0'), makeOperator('n1'), makeOperator('n2')] + shares = createShares(provider, nodes) + share0 = shares[0] + utxo = provider.addUtxo(share0.address, randomUtxo({ satoshis: 100_000n })) + }) + + it('accepts 2-of-3 signatures paying the correct nodePkh', async function () { + const fee = 1000n + const amount = 100_000n - fee + const tx = await new TransactionBuilder({ provider }) + .addInput(utxo, share0.unlock.claim(...claimArgs(nodes, [nodes[0], nodes[1]]))) + .addOutput({ to: nodes[0].addr, amount }) + .send() + + assert.ok(tx.txid) + }) + + it('rejects 1-of-3 signatures', async function () { + const fee = 1000n + const amount = 100_000n - fee + const builder = new TransactionBuilder({ provider }) + .addInput(utxo, share0.unlock.claim(...claimArgs(nodes, [nodes[0]]))) + .addOutput({ to: nodes[0].addr, amount }) + + await assert.rejects(() => builder.send(), /need at least 2-of-3|Failed to evaluate|require/i) + }) + + it('rejects payout to the wrong address', async function () { + const fee = 1000n + const amount = 100_000n - fee + const builder = new TransactionBuilder({ provider }) + .addInput(utxo, share0.unlock.claim(...claimArgs(nodes, [nodes[0], nodes[1]]))) + .addOutput({ to: nodes[1].addr, amount }) + + await assert.rejects(() => builder.send(), /output must pay nodePkh|Failed to evaluate|require/i) + }) + + it('rejects extra outputs', async function () { + const builder = new TransactionBuilder({ provider }) + .addInput(utxo, share0.unlock.claim(...claimArgs(nodes, [nodes[0], nodes[1]]))) + .addOutput({ to: nodes[0].addr, amount: 50_000n }) + .addOutput({ to: nodes[0].addr, amount: 49_000n }) + + await assert.rejects(() => builder.send(), /expected exactly 1 output|Failed to evaluate|require/i) + }) + + it('accepts signatures from nodes 1 and 2 (skipping node 0)', async function () { + const fee = 1000n + const amount = 100_000n - fee + const tx = await new TransactionBuilder({ provider }) + .addInput(utxo, share0.unlock.claim(...claimArgs(nodes, [nodes[1], nodes[2]]))) + .addOutput({ to: nodes[0].addr, amount }) + .send() + + assert.ok(tx.txid) + }) +})