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

175 lines
10 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { MetadataIndex } from '../src/adapters/metadata-index.js';
import { lineageEvents, reduce } from '../src/orgchain/lineage.js';
import { decodeLab, emptyLab, encodeLab, setRule, setTempo } from '../src/orgchain/store.js';
import { accept, act, deposit, lastHolding, lineageState, minerOf, offer, seedSample, send, take, withdraw } from '../src/orgchain/actions.js';
import { FEE, REWARD, SAT, balances, ledger, most, owners, payerOf, tanks } from '../src/orgchain/money.js';
import { askId, askStatus, dealsOf, holderOfItem, offerStatus, offerWhy } from '../src/orgchain/offers.js';
const TOPUP = 10_000 + FEE; // what one gas top-up takes from a node's money
// A lab's money as the app sees it, deals named by the lab's offers: balances by name, 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;
const deals = dealsOf(lab);
return { events, 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), deals).map(m => [m.kind, m.beat, name(m.from), name(m.to), m.amount]) };
}
const holder = (lab, item) => { const { events, live } = money(lab); return holderOfItem(lab, events, live, item)?.node.name ?? null; };
const status = (lab, o) => offerStatus(lab, lineageState(lab), o);
test('make records an item its maker holds, and the reducer vets it', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), bot = act(lab, 'birth', acme, { name: 'bot', kind: 'agent' });
send(lab, acme, bot, SAT);
const kit = act(lab, 'make', bot, { name: 'kit', note: ' A starter kit. ' });
const s = lineageState(lab);
assert.deepEqual(s.items.get(kit), { id: kit, name: 'kit', note: 'A starter kit.', maker: bot, beat: 5 });
assert.deepEqual(s.nodes.get(bot).made, [kit]);
assert.equal(s.log.at(-1).root, acme);
assert.equal(holder(lab, kit), 'bot');
assert.throws(() => act(lab, 'make', bot, { name: 'kit' }), /^Error: There is already a kit by bot\.$/);
assert.throws(() => act(lab, 'make', bot, { name: 'Kit!' }), /^Error: Names are 1-40 lowercase/);
act(lab, 'make', acme, { name: 'kit' }); // another maker may use the name
act(lab, 'close', bot, {});
assert.throws(() => act(lab, 'make', bot, { name: 'more' }), /^Error: The maker is closed\.$/);
assert.equal(lineageState(lab).items.size, 2);
});
test('an offer stands until the holder accepts; then the item and the price change hands in one transaction', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), beta = act(lab, 'found', null, { name: 'beta' });
const kit = act(lab, 'make', acme, { name: 'kit', note: 'A starter kit.' });
const before = money(lab).now, height = lab.chain.blocks.length;
const o = offer(lab, kit, beta, 2 * SAT);
assert.equal(lab.chain.mempool.length, 0); // an offer is signatures, not a transaction
assert.equal(lab.chain.blocks.length, height);
assert.equal(status(lab, o), 'open');
assert.throws(() => offer(lab, kit, beta, SAT), /^Error: beta already has an open offer on kit\. Withdraw it first\.$/);
assert.throws(() => offer(lab, kit, acme, SAT), /^Error: acme already holds kit\.$/);
assert.throws(() => offer(lab, kit, beta, 100), /^Error: Offer nothing, or at least 330 sats\.$/);
assert.throws(() => offer(lab, kit, beta, 51 * SAT), /at most 50 test BTC/);
accept(lab, o.txid);
assert.equal(lab.chain.blocks.length, height + 1);
assert.equal(holder(lab, kit), 'beta');
const after = money(lab);
assert.deepEqual(after.log.at(-1), ['deal', height + 1, 'beta', 'acme', 2 * SAT]);
assert.equal(after.now.acme, before.acme + 2 * SAT + REWARD / 2);
assert.equal(after.now.beta, before.beta - 2 * SAT - FEE + REWARD / 2);
const { tx } = lab.chain.transactions.get(o.txid);
assert.deepEqual(tx.vout.map(v => Number(v.value)), [1000, 2 * SAT, o.change]);
assert.equal(tx.vin[0].witness.length, 1); // the holder's Taproot signature on the item; the buyer's SegWit ones follow
assert.ok(tx.vin.slice(1).every(i => i.witness.length === 2));
assert.equal(status(lab, o), 'done');
assert.throws(() => accept(lab, o.txid), /already accepted/);
assert.throws(() => withdraw(lab, o.txid), /on the chain now/);
const back = offer(lab, kit, acme, 0); // a free claim back: the buyer pays only the fee
accept(lab, back.txid);
assert.equal(holder(lab, kit), 'acme');
assert.deepEqual(money(lab).log.at(-1), ['deal', height + 2, 'acme', 'beta', 0]);
});
test('payments spend unpromised coins first; an offer goes stale when a coin it promised moves', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' });
send(lab, acme, ops, SAT);
send(lab, acme, ops, SAT);
const kit = act(lab, 'make', acme, { name: 'kit' });
const o = offer(lab, kit, ops, 0.5 * SAT);
send(lab, ops, acme, 0.2 * SAT);
send(lab, ops, acme, 0.7 * SAT);
assert.equal(status(lab, o), 'open'); // both sends spent the coin the offer did not promise
send(lab, ops, acme, 0.5 * SAT);
assert.equal(status(lab, o), 'stale');
assert.equal(offerWhy(lab, lineageState(lab), o), 'ops has spent a coin the offer promised');
assert.throws(() => accept(lab, o.txid), /^Error: This offer is stale: ops has spent a coin the offer promised\.$/);
withdraw(lab, o.txid);
assert.deepEqual(lab.offers, []);
const beta = act(lab, 'found', null, { name: 'beta' }), first = offer(lab, kit, ops, 0.1 * SAT), second = offer(lab, kit, beta, SAT);
accept(lab, second.txid);
assert.equal(offerWhy(lab, lineageState(lab), first), 'kit has moved since');
});
test('a graduation leaves items at the old key: priced offers to it go stale, and it still signs for what it held', () => {
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, ops, 2 * SAT);
const kit = act(lab, 'make', ops, { name: 'kit' });
const priced = offer(lab, kit, beta, SAT);
act(lab, 'graduate', acme, { of: ops });
assert.equal(offerWhy(lab, lineageState(lab), priced), 'ops has new keys since'); // its price would reach ops's old address
assert.equal(holder(lab, kit), 'ops'); // the item stays at ops's derived key, which the lab can still sign with
withdraw(lab, priced.txid);
const again = offer(lab, kit, beta, SAT);
accept(lab, again.txid);
assert.equal(holder(lab, kit), 'beta');
assert.deepEqual(money(lab).log.at(-1).slice(2), ['beta', 'ops', SAT]);
});
test('a cued deal counts for the holder that accepted it, and offers survive a backup', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), beta = act(lab, 'found', null, { name: 'beta' });
const kit = act(lab, 'make', acme, { name: 'kit' });
setRule(lab, 'last');
setTempo(lab, 30, 1000);
const o = offer(lab, kit, beta, SAT);
const copy = decodeLab(encodeLab(lab));
assert.deepEqual(copy.offers, lab.offers);
accept(copy, o.txid, 2000);
assert.equal(lastHolding(copy), acme);
assert.deepEqual(money(copy).log.at(-1), ['deal', null, 'beta', 'acme', SAT]);
const bad = JSON.parse(encodeLab(lab));
bad.offers[0].price = 5;
assert.throws(() => decodeLab(JSON.stringify(bad)), /Invalid offer/);
});
test('on mainnet no holding mines: beats pay the lab wallet, and holdings are funded from it', () => {
const lab = emptyLab();
setRule(lab, 'none');
const acme = act(lab, 'found', null, { name: 'acme' });
assert.equal(minerOf(lab, acme), null);
assert.deepEqual(money(lab).now, {});
assert.throws(() => act(lab, 'birth', acme, { name: 'ops' }), /^Error: acme has no test BTC for gas\. Fund it from the lab wallet\.$/);
deposit(lab, acme, 5 * SAT);
assert.deepEqual(money(lab).log, [['fund', 3, null, 'acme', 5 * SAT]]);
const ops = act(lab, 'birth', acme, { name: 'ops' });
assert.throws(() => deposit(lab, ops, SAT), /Pick a holding to fund/);
assert.throws(() => deposit(lab, acme, 51 * SAT), /at most 50 test BTC/);
setTempo(lab, 'manual');
const height = lab.chain.blocks.length;
for (let i = 0; i < 4; i++) deposit(lab, acme, REWARD);
assert.ok(lab.chain.blocks.length > height); // the lab wallet runs short, and a beat refills it
assert.equal(money(lab).now.acme, 5 * SAT + 4 * REWARD - TOPUP);
assert.equal(decodeLab(encodeLab(lab)).mining.rule, 'none');
});
test('a payment can spend up to 64 coins', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' });
for (let i = 0; i < 3; i++) send(lab, acme, ops, 20 * SAT);
const txid = send(lab, ops, acme, REWARD); // three coins: more than the two the chain used to take
assert.equal(lab.chain.transactions.get(txid).tx.vin.length, 3);
for (let i = 0; i < 9; i++) send(lab, acme, ops, SAT);
const wallet = payerOf(lab.keys, lineageState(lab), lineageState(lab).nodes.get(ops));
const small = lab.chain.coins(wallet.script).filter(c => c.value === SAT);
assert.deepEqual([most(small), most(small, 8)], [9 * SAT - FEE, 8 * SAT - FEE]);
});
test('the sample ends on both sides of a deal: lineage bids for the launch kit, and well-known asks for mailr', () => {
const lab = emptyLab();
seedSample(lab);
const [o] = lab.offers, [a] = lab.asks, s = lineageState(lab), name = id => s.nodes.get(id).name;
assert.deepEqual([s.items.get(o.item).name, name(o.buyer), name(o.seller), o.price, status(lab, o)], ['launch-kit', 'lineage', 'sol', 0.1 * SAT, 'open']);
assert.deepEqual([name(a.lot), name(a.seller), a.price, askStatus(lab, s, a)], ['mailr', 'well-known', SAT, 'open']);
accept(lab, o.txid);
assert.equal(holder(lab, o.item), 'lineage');
assert.deepEqual(money(lab).log.at(-1), ['deal', 8, 'lineage', 'sol', 0.1 * SAT]);
take(lab, askId(a), o.buyer);
const t = lineageState(lab), mailr = t.nodes.get(a.lot);
assert.deepEqual([name(mailr.parent), mailr.root, mailr.index, mailr.moves.length], ['lineage', o.buyer, 0, 1]);
assert.deepEqual(money(lab).log.filter(m => m[0] === 'deal').at(-1), ['deal', 9, 'lineage', 'well-known', SAT]);
});