import test from 'node:test'; import assert from 'node:assert/strict'; import { Tx } from '@cmdcode/tapscript'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; import { LocalChain } from '../src/adapters/chain.js'; import { getVin, getVout } from '../src/upstream/primitives.js'; import { MetadataIndex } from '../src/adapters/metadata-index.js'; import { lineageEvents, reduce } from '../src/orgchain/lineage.js'; import { decodeLab, emptyLab, encodeLab, setTempo, tick } from '../src/orgchain/store.js'; import { act, minerOf, seedSample, send } from '../src/orgchain/actions.js'; import { FEE, PayWallet, REWARD, SAT, balances, brief, btc, ledger, most, owners, payerOf, satsOf } from '../src/orgchain/money.js'; const key = byte => { const secret = byte.repeat(32); return new PayWallet(secret, bytesToHex(secp256k1.getPublicKey(secret, true)), "m/84'/1'/0'/0/0"); }; // A lab's money as the app sees it: who owns each SegWit script, balances now (or at a beat), and the ledger. function money(lab, upTo) { const events = lineageEvents(new MetadataIndex(lab.chain)), live = reduce(events, { pending: true }), owner = owners(lab.keys, events, live); const named = map => Object.fromEntries([...map].map(([id, v]) => [live.nodes.get(id).name, v])); return { live, owner, now: named(balances(lab.chain, owner, upTo)), log: ledger(lab.chain, owner).map(m => [m.kind, m.beat, live.nodes.get(m.from)?.name ?? null, live.nodes.get(m.to)?.name, m.amount]) }; } test('the toy chain mines to SegWit and moves P2WPKH spends signed by the key they pay', () => { const chain = new LocalChain(), a = key('11'), b = key('22'); chain.mine(a.script); const [coin] = chain.coins(a.script); assert.equal(coin.value, REWARD); const build = (amount = SAT) => Tx.create({ vin: [getVin(coin.txid, coin.vout, coin.value, coin.scriptPubKey)], vout: [getVout(amount, b.script), getVout(coin.value - amount - FEE, a.script)] }); const signed = (signer, tx = build(), witness = null) => { tx.vin[0].witness = witness ?? signer.sign(tx, 0); return Tx.encode(tx).hex; }; const good = build(); const sig = a.sign(good, 0); assert.throws(() => chain.submitPackage([signed(b)]), /P2WPKH spends need a SIGHASH_ALL signature by the key they pay/); assert.throws(() => chain.submitPackage([signed(a, build(), [`${sig[0].slice(0, -2)}83`, sig[1]])]), /P2WPKH spends need/); assert.throws(() => chain.submitPackage([signed(a, build(), ['ab'.repeat(64)])]), /Unsupported witness/); assert.throws(() => chain.submitPackage([signed(a, build(SAT + 1), sig)]), /Invalid signature/); chain.submitPackage([signed(a, good, sig)]); chain.mine(a.script); assert.equal(chain.balance(b.script), SAT); assert.equal(chain.balance(a.script), 2 * REWARD - SAT - FEE); const copy = new LocalChain(chain.snapshot()); assert.deepEqual([copy.balance(a.script), copy.balance(b.script)], [2 * REWARD - SAT - FEE, SAT]); }); test('each beat pays the holding being worked in, and sends move test BTC down its tree', () => { const lab = emptyLab(); const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' }); const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }); assert.deepEqual(money(lab).now, { acme: 3 * REWARD }); // beats 2-4; beat 1 funded the lab wallet send(lab, acme, ops, 12.5 * SAT); send(lab, ops, bot, SAT); assert.equal(lab.chain.blocks.length, 6); assert.deepEqual(money(lab).now, { acme: 5 * REWARD - 12.5 * SAT - FEE, ops: 11.5 * SAT - FEE, bot: SAT }); assert.deepEqual(money(lab).log, [['mine', 2, null, 'acme', REWARD], ['mine', 3, null, 'acme', REWARD], ['mine', 4, null, 'acme', REWARD], ['mine', 5, null, 'acme', REWARD], ['pay', 5, 'acme', 'ops', 12.5 * SAT], ['mine', 6, null, 'acme', REWARD], ['pay', 6, 'ops', 'bot', SAT]]); assert.deepEqual(money(lab, 4).now, { acme: 3 * REWARD }); const s = money(lab).live, opsKey = payerOf(lab.keys, s, s.nodes.get(ops)); assert.match(opsKey.address, /^bcrt1q/); assert.equal(opsKey.path, "m/84'/1'/0'/0/0"); assert.equal(most(lab.chain.coins(opsKey.script)), 11.5 * SAT - 2 * FEE); const before = encodeLab(lab); assert.throws(() => send(lab, bot, ops, 2 * SAT), /^Error: bot has 1 tBTC, and sending 2 tBTC also costs the 500-sat fee\.$/); assert.throws(() => send(lab, acme, acme, SAT), /cannot send to itself/); assert.throws(() => send(lab, acme, ops, 51 * SAT), /at most 50 test BTC/); assert.throws(() => send(lab, acme, ops, 100), /at least 330 sats/); assert.throws(() => send(lab, ops, bot, 11.5 * SAT - 2 * FEE - 100), /^Error: Sending 11\.499989 tBTC would leave 100 sats of change, under the 330-sat floor\. Send 11\.49999 tBTC, or at least 230 sats less\.$/); assert.equal(encodeLab(lab), before); const other = act(lab, 'found', null, { name: 'other' }); assert.throws(() => send(lab, acme, other, SAT), /inside one holding/); assert.equal(money(lab).now.other, REWARD); // its founding beat paid it const copy = decodeLab(encodeLab(lab)); assert.deepEqual(money(copy).now, money(lab).now); }); test('a timed tempo cues sends; the beat that lands them pays whoever the clock is told', () => { const lab = emptyLab(); const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' }); setTempo(lab, 10, 1000); send(lab, acme, ops, SAT, 2000); assert.deepEqual(lab.clock, { tempo: 10, nextAt: 12000 }); assert.deepEqual(money(lab).log.at(-1), ['pay', null, 'acme', 'ops', SAT]); assert.equal(money(lab).now.ops, SAT); // now includes what is cued assert.equal(money(lab, 3).now.ops, undefined); assert.equal(tick(lab, 12000, minerOf(lab, acme)), true); assert.deepEqual(money(lab).log.slice(-2), [['mine', 4, null, 'acme', REWARD], ['pay', 4, 'acme', 'ops', SAT]]); // a coinbase leads its block assert.equal(tick(lab, 99999, minerOf(lab, acme)), false); // nothing cued: no empty beats }); test('closed nodes neither send nor receive, and a closed holding earns nothing', () => { const lab = emptyLab(); const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' }); const lab2 = act(lab, 'birth', acme, { name: 'lab' }); send(lab, acme, ops, SAT); act(lab, 'close', ops, {}); assert.throws(() => send(lab, acme, ops, SAT), /ops is closed/); assert.throws(() => send(lab, ops, lab2, 1000), /ops is closed/); const paid = money(lab).now.acme; act(lab, 'close', acme, {}); assert.equal(money(lab).now.acme, paid); // the beat that closed it paid the lab wallet assert.equal(minerOf(lab, acme), null); }); test('graduation moves money to the new keys; the sample shows it', () => { const lab = emptyLab(); seedSample(lab); const { now, log, live } = money(lab); assert.deepEqual(now, { yolopreneur: 300 * SAT - 50 * SAT - 2 * FEE, 'well-known': 8 * SAT - FEE, lineage: 2 * SAT - FEE, time: 38.5 * SAT - 2 * FEE, sol: SAT, luna: 0.5 * SAT }); assert.deepEqual(log.at(-1), ['move', 7, 'lineage', 'lineage', 2 * SAT - FEE]); const lineage = [...live.nodes.values()].find(n => n.name === 'lineage'); assert.equal(lab.chain.balance(payerOf(lab.keys, live, lineage).script), 2 * SAT - FEE); assert.deepEqual(money(lab, 5).now.lineage, 2 * SAT); // before graduating, at its derived address }); test('amounts read and print as test BTC', () => { assert.deepEqual(['1.5', '0.00000001', '.5', '7', ' 2.25 ', '1.123456789', '', 'abc', '1e3', '-1'].map(satsOf), [1.5 * SAT, 1, 0.5 * SAT, 7 * SAT, 2.25 * SAT, null, null, null, null, null]); assert.deepEqual([4999999500, 0, REWARD, 1].map(btc), ['49.999995 tBTC', '0 tBTC', '50 tBTC', '0.00000001 tBTC']); assert.deepEqual([4999999500, 1250000000, 125000000, 99999, 1149999500].map(brief), ['≈50 tBTC', '12.5 tBTC', '1.25 tBTC', '0.00099999 tBTC', '≈11.5 tBTC']); });