Files
teahubxyzandClaude Opus 5.5 74ed801176 Orgchain v1.13: whole trees change hands
An org now changes hands with its tree, the derived orgs and agents below
it and the items they hold, and a holding changes hands whole, sold by its
owner, whom the lab wallet plays (the price goes there). Sales stay
cash-free: the money in the tree, every gas tank included, goes to the
seller first, and the items go with the tree.

An ask signs one input, so listing a tree bundles every inscription in it
into one output at the lot's key; an offer spends every output the tree sits
in and merges them. After the join and the adopt, which now move a holding
too (it becomes its buyer's next org), the buyer splits the output back, one
inscription to an output at its new key, each item with the node that held
it, by the layout the deal recorded. A listed or sold tree is frozen whole:
nothing in it acts, sends, is funded or sells alone, nothing above it
graduates or closes it, and a listed holding mines nothing. Graduation roots
are kept by graduation event, since a node can graduate, move and graduate
again. Transactions take up to 64 inputs and outputs.

Rehearsed on Bitcoin Core regtest from height 144: sixteen beats and 131
transactions, time with its six below it bought on an offer, and acme sold
whole on an ask, eleven inscriptions in one output, then split again; ord
puts every inscription in the same output as the lab.

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

187 lines
12 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { Signer, Tx } from '@cmdcode/tapscript';
import { TestWallet } from '../src/adapters/wallet.js';
import { secp256k1 } from '@noble/curves/secp256k1';
import { bytesToHex } from '@noble/curves/abstract/utils';
import { LocalChain, MAX_IO } 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, miningPreference, setRigs, setRule, setTempo, tick } from '../src/orgchain/store.js';
import { act, gasCheck, lastHolding, lineageState, minerOf, seedSample, send } from '../src/orgchain/actions.js';
import { FEE, PayWallet, REWARD, SAT, balances, brief, btc, ledger, most, owners, payerOf, satsOf, tankOf, tanks } from '../src/orgchain/money.js';
import { keyOf } from '../src/orgchain/keys.js';
const TOPUP = 10_000 + FEE; // what one gas top-up takes from a node's money
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: balances now (or at a beat), and the ledger as [kind, beat, from, to, amount].
function money(lab, upTo) {
const events = lineageEvents(new MetadataIndex(lab.chain)), live = reduce(events, { pending: true });
const owner = owners(lab.keys, events, live), name = id => live.nodes.get(id)?.name ?? null;
return { live, now: Object.fromEntries([...balances(lab.chain, owner, upTo)].map(([id, v]) => [name(id), v])),
log: ledger(lab.chain, owner, tanks(lab.keys, events, live)).map(m => [m.kind, m.beat, name(m.from), name(m.to), m.amount]) };
}
test('the toy chain mines to SegWit, splits a coinbase, 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);
chain.mineTo([getVout(REWARD / 2, a.script), getVout(REWARD / 2, b.script)]);
assert.throws(() => chain.mineTo([getVout(REWARD, a.script), getVout(1000, b.script)]), /Invalid local coinbase/);
assert.equal(chain.blocks.length, 3);
const copy = new LocalChain(chain.snapshot());
assert.deepEqual([copy.balance(a.script), copy.balance(b.script)], [2.5 * REWARD - SAT - FEE, SAT + REWARD / 2]);
});
test('ordinary spends take up to 64 inputs and outputs, and a key-path signature may be SINGLE|ANYONECANPAY', () => {
const chain = new LocalChain(), seller = TestWallet.create(), buyer = TestWallet.create();
chain.mine(seller.script);
chain.mine(buyer.script);
const [s] = chain.coins(seller.script), [b] = chain.coins(buyer.script), vin = c => getVin(c.txid, c.vout, c.value, c.scriptPubKey);
const part = SAT / 2, fan = n => {
const tx = Tx.create({ vin: [vin(b)], vout: Array.from({ length: n }, (_, i) => getVout(i < n - 1 ? part : b.value - (n - 1) * part - FEE, buyer.script)) });
tx.vin[0].witness = [buyer.sign(tx)];
return Tx.encode(tx).hex;
};
assert.equal(MAX_IO, 64);
assert.throws(() => chain.submitPackage([fan(MAX_IO + 1)]), /Unsupported transaction shape/);
chain.submitPackage([fan(MAX_IO)]);
assert.equal(chain.coins(buyer.script).length, MAX_IO);
// A sell order: the seller signs its coin and the output it wants paid, and nothing else; a buyer fills it later.
const order = Tx.create({ vin: [vin(s)], vout: [getVout(s.value - FEE, seller.script)] });
const sig = Signer.taproot.sign(seller.secret, order, 0, { sigflag: 0x83 }).hex;
assert.match(sig, /^[0-9a-f]{128}83$/);
const [c] = chain.coins(buyer.script), fill = (paid = s.value - FEE) => {
const tx = Tx.create({ vin: [vin(s), vin(c)], vout: [getVout(paid, seller.script), getVout(c.value - (paid - (s.value - FEE)), buyer.script)] });
tx.vin[0].witness = [sig];
tx.vin[1].witness = [buyer.sign(tx, undefined, 1)];
return Tx.encode(tx).hex;
};
assert.throws(() => chain.submitPackage([fill(s.value - FEE - 1000)]), /Invalid signature/); // the seller's output is fixed
chain.submitPackage([fill()]);
chain.mine(buyer.script);
assert.equal(chain.balance(seller.script), REWARD - FEE);
});
test('every act pays its own gas, and an unfunded node is stopped before anything is signed', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' });
const before = encodeLab(lab);
assert.throws(() => act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }), /^Error: ops has no test BTC for gas\. acme can send some\.$/);
assert.throws(() => act(lab, 'decide', ops, { title: 'Ship it' }), /ops has no test BTC for gas/);
assert.equal(encodeLab(lab), before);
send(lab, acme, ops, 12.5 * SAT);
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' });
send(lab, ops, bot, SAT);
const { now, log } = money(lab);
assert.deepEqual(now, { acme: 5 * REWARD - 12.5 * SAT - TOPUP - FEE, ops: 11.5 * SAT - TOPUP - FEE, bot: SAT });
assert.deepEqual(log.filter(([kind]) => kind !== 'mine'), [['gas', 3, 'acme', 'acme', 10_000], ['pay', 4, 'acme', 'ops', 12.5 * SAT],
['gas', 5, 'ops', 'ops', 10_000], ['pay', 6, 'ops', 'bot', SAT]]);
const s = lineageState(lab), tank = name => tankOf(lab.chain, keyOf(lab.keys, s, [...s.nodes.values()].find(n => n.name === name)).wallet);
assert.deepEqual([tank('acme'), tank('ops')], [7830, 7500]); // a birth burns three fees and hands its postage to the child
assert.equal(gasCheck(lab, s, 'decide', bot, {}), '');
const opsKey = payerOf(lab.keys, s, s.nodes.get(ops));
assert.equal(most(lab.chain.coins(opsKey.script)), 11.5 * SAT - TOPUP - 2 * FEE);
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, ops, bot, 11.5 * SAT - TOPUP - 2 * FEE - 100),
/^Error: Sending 11\.499884 tBTC would leave 100 sats of change, under the 330-sat floor\. Send 11\.499885 tBTC, or at least 230 sats less\.$/);
});
test('beats split across holdings by hashrate, or go whole to the last act', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), beta = act(lab, 'found', null, { name: 'beta' });
const mined = beat => money(lab).log.filter(([kind, b]) => kind === 'mine' && b === beat).map(([, , , to, amount]) => [to, amount]);
assert.deepEqual(mined(2), [['acme', REWARD]]);
assert.deepEqual(mined(3), [['acme', REWARD / 2], ['beta', REWARD / 2]]); // beta's founding beat already splits
setRigs(lab, acme, 3);
act(lab, 'decide', beta, { title: 'Mine more' });
assert.deepEqual(mined(4), [['acme', 37.5 * SAT], ['beta', 12.5 * SAT]]);
setRigs(lab, beta, 0);
act(lab, 'decide', acme, { title: 'Alone' });
assert.deepEqual(mined(5), [['acme', REWARD]]);
setRule(lab, 'last');
setRigs(lab, beta, 1);
act(lab, 'decide', beta, { title: 'Clicked' });
assert.deepEqual(mined(6), [['beta', REWARD]]);
assert.equal(minerOf(lab, acme).length, 1);
setTempo(lab, 30, 1000);
act(lab, 'decide', acme, { title: 'First' }, 2000);
act(lab, 'decide', beta, { title: 'Last in' }, 3000);
assert.equal(lastHolding(lab), beta);
assert.equal(tick(lab, 32000, minerOf(lab, lastHolding(lab))), true);
assert.deepEqual(mined(7), [['beta', REWARD]]); // the race for a timed beat: whoever acted last takes it
assert.equal(lastHolding(lab), null);
setTempo(lab, 'each');
assert.deepEqual(decodeLab(encodeLab(lab)).mining, { rule: 'last', rigs: { [acme]: 3 } });
assert.throws(() => miningPreference({ rule: 'luck' }), /Invalid mining rule/);
assert.throws(() => miningPreference({ rigs: { [acme]: 10 } }), /Invalid hashrate/);
const three = emptyLab();
for (const name of ['a', 'b', 'c']) act(three, 'found', null, { name });
assert.deepEqual(money(three).log.filter(([kind, b]) => kind === 'mine' && b === 4).map(m => m[4]), [1666666666, 1666666666, 1666666668]);
});
test('sends cross holdings and cue at a timed tempo; closed nodes neither send nor receive', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' });
const beta = act(lab, 'found', null, { name: 'beta' });
send(lab, acme, beta, 2 * SAT);
assert.deepEqual(money(lab).log.at(-1), ['pay', 5, 'acme', 'beta', 2 * SAT]);
setTempo(lab, 10, 1000);
send(lab, beta, ops, SAT, 2000);
assert.deepEqual(lab.clock, { tempo: 10, nextAt: 12000 });
assert.deepEqual(money(lab).log.at(-1), ['pay', null, 'beta', 'ops', SAT]);
assert.equal(money(lab).now.ops, SAT); // now includes what is cued
assert.equal(money(lab, 5).now.ops, undefined);
assert.equal(tick(lab, 12000, minerOf(lab, acme)), true);
assert.deepEqual(money(lab).log.slice(-3).map(m => m.slice(0, 4)), [['mine', 6, null, 'acme'], ['mine', 6, null, 'beta'], ['pay', 6, 'beta', 'ops']]);
assert.equal(tick(lab, 99999, minerOf(lab, acme)), false); // nothing cued: no empty beats
setTempo(lab, 'each');
act(lab, 'close', ops, {});
assert.throws(() => send(lab, acme, ops, SAT), /ops is closed/);
assert.throws(() => send(lab, ops, beta, 1000), /ops is closed/);
assert.throws(() => send(lab, acme, acme, SAT), /cannot send to itself/);
assert.throws(() => send(lab, acme, beta, 51 * SAT), /at most 50 test BTC/);
assert.throws(() => send(lab, acme, beta, 100), /at least 330 sats/);
act(lab, 'close', beta, {});
assert.equal(minerOf(lab, acme).length, 1); // a closed holding stops mining
});
test('graduation moves money to the new keys, and the graduate pays its own claim; the sample shows it', () => {
const lab = emptyLab();
seedSample(lab);
const { now, log, live } = money(lab);
// lineage graduates at beat 7 into a holding of its own, so it mines half of that beat. Its keyring at beat 6 filled
// its old gas tank, which stays with its old keys; its acts after graduating fill a new one.
assert.deepEqual(now, { yolopreneur: 275 * SAT - 50 * SAT - 2 * FEE - 2 * TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
lineage: 27 * SAT - FEE - 2 * TOPUP, time: 38.5 * SAT - 2 * FEE - 3 * TOPUP, sol: SAT - TOPUP, luna: 0.5 * SAT });
assert.deepEqual(log.slice(-2), [['move', 7, 'lineage', 'lineage', 2 * SAT - TOPUP - FEE], ['gas', 7, 'lineage', 'lineage', 10_000]]);
const lineage = [...live.nodes.values()].find(n => n.name === 'lineage');
assert.equal(lab.chain.balance(payerOf(lab.keys, live, lineage).script), 27 * SAT - FEE - 2 * TOPUP);
assert.equal(money(lab, 5).now.lineage, 2 * SAT); // before graduating, at its derived address
const solo = emptyLab();
const root = act(solo, 'found', null, { name: 'acme' }), kid = act(solo, 'birth', root, { name: 'kid' });
assert.throws(() => act(solo, 'graduate', root, { of: kid }), /^Error: kid needs test BTC of its own to claim its new root\. Send it some first\.$/);
});
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']);
});