Files
orgchain/tests/holdings.test.js
T
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

152 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { MetadataIndex } from '../src/adapters/metadata-index.js';
import { holderCoin } from '../src/adapters/handoff.js';
import { lineageEvents, reduce, summary } from '../src/orgchain/lineage.js';
import { decodeLab, emptyLab, encodeLab, setRule } from '../src/orgchain/store.js';
import { accept, act, adopt, deposit, lineageState, offer, sell, send, take, unlist, withdraw } from '../src/orgchain/actions.js';
import { SAT, balances, ledger, miners, owners, tanks } from '../src/orgchain/money.js';
import { holderOf, keyOf, scriptHex } from '../src/orgchain/keys.js';
import { askId, decodeOffer, dealsOf, holderOfItem, offerWhy } from '../src/orgchain/offers.js';
// Whole trees change hands: a holding, sold whole by its owner (the lab wallet plays it), and an org with its derived tree
// and the items they hold. Under the 'none' rule no holding mines, so every balance moves only by what the test does.
function money(lab) {
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 { now: Object.fromEntries([...balances(lab.chain, owner)].map(([id, v]) => [name(id), v])),
log: ledger(lab.chain, owner, tanks(lab.keys, events, live), dealsOf(lab)).map(m => [m.kind, m.beat, name(m.from), name(m.to), m.amount]) };
}
const holder = (lab, id) => { const events = lineageEvents(new MetadataIndex(lab.chain)); return holderOfItem(lab, events, lineageState(lab), id)?.node.name ?? null; };
const alone = (lab, id) => holderCoin(lab.chain, id).inscriptions.length === 1;
const errors = (list, why) => list.forEach(run => assert.throws(run, e => e.message === why, why));
function twoHoldings() {
const lab = emptyLab();
setRule(lab, 'none');
const acme = act(lab, 'found', null, { name: 'acme' }), beta = act(lab, 'found', null, { name: 'beta' });
deposit(lab, acme, 10 * SAT);
deposit(lab, beta, 10 * SAT);
return { lab, acme, beta };
}
test('a holding sells whole on an ask: its owner lists it cash-free, its tree travels as one coin, and its buyer adopts it', () => {
const { lab, acme, beta } = twoHoldings(), ops = act(lab, 'birth', acme, { name: 'ops' });
send(lab, acme, ops, SAT);
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' });
send(lab, ops, bot, SAT / 2);
const kit = act(lab, 'make', ops, { name: 'kit' }), tool = act(lab, 'make', bot, { name: 'tool' });
act(lab, 'keyring', ops, {});
const wallet = scriptHex(lab.wallet.script);
// Its owner lists it: the tree's money goes to the lab wallet, and its five inscriptions become one coin at acme's key.
const a = sell(lab, acme, 5 * SAT), listed = lab.chain.blocks.length, m = money(lab);
assert.deepEqual([a.seller, a.pay, m.now.acme ?? 0, m.now.ops ?? 0, m.now.bot ?? 0], [null, wallet, 0, 0, 0]);
assert.deepEqual(m.log.filter(x => x[1] === listed).map(x => [x[0], x[2], x[3]]), [['pay', 'acme', null], ['pay', 'ops', null], ['pay', 'bot', null]]);
assert.deepEqual(lab.chain.utxos.get(askId(a)).inscriptions.map(i => i.id), [acme, ops, bot, kit, tool]);
assert.deepEqual([a.layout, holder(lab, kit)], [[[kit, ops], [tool, bot]], 'acme']);
assert.deepEqual(decodeLab(encodeLab(lab)).asks, lab.asks);
// Listed, the whole tree is frozen, and what's in it doesn't sell alone.
errors([() => act(lab, 'decide', bot, { title: 'x' }), () => send(lab, beta, ops, SAT), () => sell(lab, ops, SAT)],
'acme is listed for sale at 5 tBTC. Withdraw its ask first.');
errors([() => offer(lab, kit, beta, SAT)], "Kit goes with acme's tree while it changes hands.");
errors([() => offer(lab, ops, beta, SAT)], "Ops goes with acme's tree while it changes hands.");
// Withdrawn, the coin splits back: each inscription at its own key, each item with whoever held it.
unlist(lab, askId(a));
assert.deepEqual([acme, ops, bot, kit, tool].map(id => alone(lab, id)), [true, true, true, true, true]);
assert.deepEqual([holder(lab, kit), holder(lab, tool), holderOf(lab, lineageState(lab), bot).who], ['ops', 'bot', 'own']);
// Listed again, beta takes it: the deal, the handshake and the split, in one beat. acme is beta's first org now.
const b = sell(lab, acme, 5 * SAT);
take(lab, askId(b), beta);
const s = lineageState(lab), node = id => s.nodes.get(id), deal = lab.chain.transactions.get(b.taken.txid).tx;
assert.deepEqual([node(acme).kind, node(acme).parent, node(acme).root, node(acme).index, s.roots.map(r => r.name)], ['org', beta, beta, 0, ['beta']]);
assert.deepEqual([acme, ops, bot].map(id => [node(id).root, keyOf(lab.keys, s, node(id)).from, holderOf(lab, s, id).who, alone(lab, id)]),
[[beta, "beta root › 0'", 'own', true], [beta, "beta root › 0' › 0'", 'own', true], [beta, "beta root › 0' › 0'", 'own', true]]);
assert.deepEqual([holder(lab, kit), holder(lab, tool), alone(lab, kit), alone(lab, tool)], ['ops', 'bot', true, true]);
assert.deepEqual([node(ops).keyring.retired.cause, scriptHex(deal.vout[1].scriptPubKey)], ['move', wallet]); // the price went to its owner
assert.deepEqual(money(lab).log.filter(x => x[0] === 'deal'), [['deal', lab.chain.blocks.length, 'beta', null, 5 * SAT]]);
assert.equal(summary(s.log.find(e => e.op === 'join'), s), 'join acme to beta, a holding till now');
// It acts again as beta's, on beta's money.
send(lab, beta, acme, SAT);
act(lab, 'decide', acme, { title: 'Under new owners' });
assert.equal(lineageState(lab).nodes.get(acme).decisions[0].title, 'Under new owners');
});
test('an org sells with its tree on an offer: every coin it sits in goes in, one comes out, and it splits back once adopted', () => {
const { lab, acme, beta } = twoHoldings(), ops = act(lab, 'birth', acme, { name: 'ops' });
send(lab, acme, ops, SAT);
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }), sub = act(lab, 'birth', ops, { name: 'sub' });
send(lab, ops, bot, SAT / 2);
const tool = act(lab, 'make', bot, { name: 'tool' }), o = offer(lab, ops, beta, 2 * SAT);
assert.deepEqual([o.seller, o.holders.map(h => h.ids), o.layout], [acme, [[ops], [bot], [sub], [tool]], [[tool, bot]]]);
// An item bought into the tree since would stay behind, so the offer goes stale; so does a node of the tree acting.
const gift = act(lab, 'make', acme, { name: 'gift' });
take(lab, askId(sell(lab, gift, SAT / 10)), bot);
assert.equal(offerWhy(lab, lineageState(lab), o), "ops's tree has changed since");
withdraw(lab, o.txid);
const o2 = offer(lab, ops, beta, 2 * SAT);
assert.deepEqual(o2.layout, [[tool, bot], [gift, bot]]);
act(lab, 'decide', bot, { title: 'Busy' });
assert.equal(offerWhy(lab, lineageState(lab), o2), "bot's inscription has moved since");
withdraw(lab, o2.txid);
const o3 = offer(lab, ops, beta, 2 * SAT);
assert.deepEqual(decodeLab(encodeLab(lab)).offers, lab.offers);
// acme accepts: the tree's money goes home first, then its five coins go in and leave as one, at beta's key.
accept(lab, o3.txid);
const merged = lab.chain.utxos.get(`${o3.txid}:0`), m = money(lab);
assert.equal(lab.chain.transactions.get(o3.txid).tx.vin.length, 5 + o3.payers.length);
assert.deepEqual([merged.inscriptions.map(i => i.id), scriptHex(merged.scriptPubKey)], [[ops, bot, sub, tool, gift], o3.to]);
assert.deepEqual([m.now.ops ?? 0, m.now.bot ?? 0, m.log.at(-1)], [0, 0, ['deal', lab.chain.blocks.length, 'beta', 'acme', 2 * SAT]]);
errors([() => act(lab, 'decide', bot, { title: 'x' }), () => act(lab, 'graduate', acme, { of: ops }), () => act(lab, 'close', acme)],
'ops was sold to beta, which adopts it next.');
errors([() => offer(lab, sub, acme, SAT)], "Sub goes with ops's tree while it changes hands.");
// beta adopts it, and the coin splits back by the offer's layout, every inscription at its new key.
adopt(lab, ops, beta);
const s = lineageState(lab), node = id => s.nodes.get(id);
assert.deepEqual([ops, bot, sub].map(id => [node(id).root, keyOf(lab.keys, s, node(id)).from, holderOf(lab, s, id).who, alone(lab, id)]),
[[beta, "beta root › 0'", 'own', true], [beta, "beta root › 0'", 'own', true], [beta, "beta root › 0' › 0'", 'own', true]]);
assert.deepEqual([holder(lab, tool), holder(lab, gift), alone(lab, tool), alone(lab, gift)], ['bot', 'bot', true, true]);
});
test('a tree sells only whole and frozen: nothing in it listed alone, nothing bought left to adopt, nothing above acting on it', () => {
const { lab, acme, beta } = twoHoldings(), ops = act(lab, 'birth', acme, { name: 'ops' });
send(lab, acme, ops, SAT);
const sub = act(lab, 'birth', ops, { name: 'sub' }), kit = act(lab, 'make', ops, { name: 'kit' });
const ka = sell(lab, kit, SAT);
errors([() => sell(lab, ops, 2 * SAT), () => sell(lab, acme, 2 * SAT)], 'Kit is listed on its own at 1 tBTC. Withdraw its ask first.');
unlist(lab, askId(ka));
const sa = sell(lab, sub, SAT);
errors([() => sell(lab, ops, 2 * SAT), () => offer(lab, ops, beta, 2 * SAT)], 'Sub is listed on its own at 1 tBTC. Withdraw its ask first.');
unlist(lab, askId(sa));
const a = sell(lab, ops, 2 * SAT);
errors([() => act(lab, 'decide', sub, { title: 'x' }), () => act(lab, 'graduate', ops, { of: sub }), () => act(lab, 'graduate', acme, { of: ops }),
() => act(lab, 'close', acme), () => sell(lab, sub, SAT)], 'ops is listed for sale at 2 tBTC. Withdraw its ask first.');
unlist(lab, askId(a));
// ops buys an org of beta's and has not adopted it: its holding cannot sell until it does.
send(lab, acme, ops, SAT);
const far = act(lab, 'birth', beta, { name: 'far' });
accept(lab, offer(lab, far, ops, SAT / 10).txid);
errors([() => sell(lab, acme, 3 * SAT)], 'Ops holds far, which it bought, and adopts it first.');
adopt(lab, far, ops);
const b = sell(lab, acme, 3 * SAT);
assert.deepEqual(lab.chain.utxos.get(askId(b)).inscriptions.map(i => i.id), [acme, ops, sub, far, kit]);
// A coin saved by an offer before v1.13 was its one holder.
const { holders, layout, ...old } = offer(lab, acme, beta, SAT);
assert.deepEqual([decodeOffer({ ...old, holder: holders[0] }).holders, layout], [[holders[0]], [[kit, ops]]]);
});
test('a holding changing hands mines nothing, so no beat pays keys it is leaving', () => {
const lab = emptyLab(), acme = act(lab, 'found', null, { name: 'acme' }), beta = act(lab, 'found', null, { name: 'beta' });
const mining = () => miners(lab, lineageState(lab)).map(r => r.name);
assert.deepEqual(mining(), ['acme', 'beta']);
const a = sell(lab, acme, SAT), had = money(lab).now.acme ?? 0;
assert.deepEqual([mining(), had], [['beta'], 0]);
assert.throws(() => deposit(lab, acme, SAT), /^Error: acme is listed for sale at 1 tBTC\. Withdraw its ask first\.$/);
act(lab, 'decide', beta, { title: 'Keep mining' });
assert.equal(money(lab).now.acme ?? 0, 0);
unlist(lab, askId(a));
assert.deepEqual(mining(), ['acme', 'beta']);
accept(lab, offer(lab, acme, beta, SAT).txid); // sold, and waiting for beta to adopt it
assert.deepEqual(mining(), ['beta']);
adopt(lab, acme, beta);
assert.deepEqual([mining(), lineageState(lab).nodes.get(acme).root], [['beta'], beta]);
});