Files
orgchain/tests/money.test.js
T
teahubxyzandClaude Opus 5.5 6d5806d45b Orgchain v1.5: beats pay the holding, and test BTC moves down the tree
Each beat pays its 50 test BTC block reward to the holding being worked in, at
its SegWit (BIP-84) address. Send moves test BTC between active nodes of a
holding, signed by the sender's BIP-84 key, with the toy chain's 500-sat fee:
at most two coins in and one block reward out. Sends land like acts, and Beat
now mines a beat even when nothing is cued. A graduation moves each re-keyed
node's money to its new address with its inscription. Balances follow the
slider, the timeline lists every reward and send, and Inspect shows exact
amounts and the raw transaction. The sample passes some of yolopreneur's
rewards down its tree.

The engine's one local change: the toy chain also takes P2WPKH outputs and
SIGHASH_ALL P2WPKH spends (BIP-143), in src/adapters/chain.js. Super
Testnet's tests pass unchanged; tests/money.test.js covers the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
2026-10-14 22:25:38 -07:00

122 lines
7.7 KiB
JavaScript

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']);
});