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