import test from 'node:test'; import assert from 'node:assert/strict'; import { MetadataIndex } from '../src/adapters/metadata-index.js'; import { PROTOCOL, census, kidsOf, lineageEvents, reduce, rows, standing, summary } from '../src/orgchain/lineage.js'; import { decodeLab, emptyLab, encodeLab, setRule } from '../src/orgchain/store.js'; import { accept, act, adopt as adoptBought, deposit, frozenWhy, lineageState, offer, sell, send, take, unlist, withdraw } from '../src/orgchain/actions.js'; import { FEE, SAT, balances, ledger, owners, tanks } from '../src/orgchain/money.js'; import { holderOf, keyOf } from '../src/orgchain/keys.js'; import { DUMMY, askId, askStatus, askWhy, dealsOf, holderOfItem, offerWhy } from '../src/orgchain/offers.js'; let n = 0; const ev = (op, parent = null, meta = {}, height = 1) => ({ id: `${String(++n).padStart(64, '0')}i0`, status: height === null ? 'pending' : 'confirmed', height, metadata: { op, ...meta }, parent, protocol: PROTOCOL }); const join = (node, to, height = 2) => ev('join', node.id, { to: to.id ?? to }, height); const adopt = (to, node, height = 2) => ev('adopt', to.id, { of: node.id ?? node }, height); const why = (s, e) => { const entry = s.log.find(x => x.id === e.id); return entry.ok ? 'ok' : entry.why; }; test('an org changes hands by a handshake: it joins a new parent, which adopts it, in either order', () => { const acme = ev('found', null, { name: 'acme' }), ops = ev('birth', acme.id, { name: 'ops' }), bot = ev('birth', ops.id, { name: 'bot', kind: 'agent' }); const kit = ev('birth', acme.id, { name: 'kit' }), beta = ev('found', null, { name: 'beta' }), ghost = 'f'.repeat(64) + 'i0'; const tries = [ [adopt(beta, bot), 'an agent changes hands only with the org it sits in'], [join(kit, bot), 'agents adopt only once they graduate'], [join(kit, acme), "kit is already acme's"], [join(kit, kit), 'kit cannot join itself or anything below it'], [join(acme, ops), 'acme cannot join itself or anything below it'], [adopt(beta, ghost), 'that node is not in this lineage'], [join(kit, ghost), 'the new parent must be a node in this lineage'], ]; const base = [acme, ops, bot, kit, beta], j = join(kit, beta, 3), a = adopt(beta, kit, 3); const s = reduce([...base, ...tries.map(([e]) => e), j, a]); tries.forEach(([e, want]) => assert.equal(why(s, e), want)); const k = s.nodes.get(kit.id), entry = id => s.log.find(x => x.id === id); assert.deepEqual([k.parent, k.root, k.edge, k.index, k.joining], [beta.id, beta.id, 'derived', 0, null]); assert.deepEqual(k.moves, [{ from: acme.id, index: 1, to: beta.id, beat: 3, join: j.id, adopt: a.id }]); assert.deepEqual([s.nodes.get(acme.id).children.includes(kit.id), kidsOf(s.nodes, s.nodes.get(acme.id)).map(x => x.name)], [true, ['ops']]); assert.deepEqual([entry(j.id).roots, entry(a.id).roots], [[acme.id, beta.id], [beta.id, acme.id]]); assert.deepEqual([summary(entry(j.id), s), summary(entry(a.id), s)], ['join kit to beta, from acme', 'adopt kit into beta, from acme']); // A miner may put the two halves in either order: the same tree, and the same record of each, either way. const t = reduce([...base, ...tries.map(([e]) => e), a, j]), view = (st, id) => { const x = st.log.find(e => e.id === id); return [x.ok, x.root, x.roots, x.node]; }; assert.deepEqual(t.nodes.get(kit.id), k); assert.deepEqual([view(t, j.id), view(t, a.id)], [view(s, j.id), view(s, a.id)]); // Alone, a half waits for the other. const half = reduce([...base, j]), other = reduce([...base, a]); assert.deepEqual([summary(half.log.at(-1), half), summary(other.log.at(-1), other)], ['join kit to beta, for beta to adopt', 'adopt kit into beta, once kit joins']); assert.deepEqual([half.nodes.get(kit.id).parent, other.nodes.get(kit.id).parent], [acme.id, acme.id]); // acme's rows keep kit at its old index, away; beta's draw it at its new one. assert.deepEqual(rows(s, acme.id).filter(r => r.type === 'node').map(r => [r.node.name, r.path, r.away, r.moved]), [['acme', '●', false, false], ['ops', "0'", false, false], ['bot', "0'", false, false], ['kit', "1'", true, true]]); assert.deepEqual(rows(s, beta.id).filter(r => r.type === 'node').map(r => [r.node.name, r.path, r.away]), [['beta', '●', false], ['kit', "0'", false]]); // Its old index is never reused, and its name is free again under acme. const u = reduce([...base, j, a, ev('birth', acme.id, { name: 'kit' }, 4)]), again = u.log.at(-1); assert.deepEqual([again.ok, u.nodes.get(again.id).index], [true, 2]); }); test('a joining org cannot graduate; a close leaves an org that moved away alone; a graduate can come back', () => { const acme = ev('found', null, { name: 'acme' }), kit = ev('birth', acme.id, { name: 'kit' }), beta = ev('found', null, { name: 'beta' }); const j = join(kit, beta), held = ev('graduate', acme.id, { of: kit.id }, 2), a = adopt(beta, kit, 3), shut = ev('close', acme.id, {}, 4); const s = reduce([acme, kit, beta, j, held, a, shut]); assert.equal(why(s, held), 'kit is joining beta'); assert.deepEqual([s.nodes.get(kit.id).status, s.log.at(-1).also], ['active', []]); // Under beta, kit graduates into its own holding, then joins acme again, as acme's next org: its first index stays // unused. It can graduate again from there, with a fresh root of its own. const g = ev('graduate', beta.id, { of: kit.id }, 4), t = reduce([acme, kit, beta, j, a, g, join(kit, acme, 5), adopt(acme, kit, 5)]); const k = t.nodes.get(kit.id); assert.ok(t.log.every(e => e.ok), t.log.filter(e => !e.ok).map(e => e.why).join('; ')); assert.deepEqual([k.parent, k.root, k.edge, k.index, k.rooted, t.roots.map(r => r.name)], [acme.id, acme.id, 'derived', 1, g.id, ['acme', 'beta']]); assert.deepEqual(rows(t, acme.id).filter(r => r.type === 'node').map(r => [r.node.name, r.path, r.moved]), [['acme', '●', false], ['kit', "0'", true], ['kit', "1'", false]]); const again = ev('graduate', acme.id, { of: kit.id }, 6), v = reduce([acme, kit, beta, j, a, g, join(kit, acme, 5), adopt(acme, kit, 5), again]); assert.deepEqual([v.log.at(-1).ok, v.nodes.get(kit.id).rooted, v.roots.map(r => r.name)], [true, again.id, ['acme', 'beta', 'kit']]); // A later join replaces a pending one; an adoption waits for its own join. const gamma = ev('found', null, { name: 'gamma' }); const u = reduce([acme, kit, beta, gamma, join(kit, beta), join(kit, gamma), adopt(beta, kit, 3)]); assert.deepEqual([u.nodes.get(kit.id).parent, u.nodes.get(kit.id).joining.to, u.nodes.get(kit.id).moves], [acme.id, gamma.id, []]); }); test('a holding changes hands whole: it joins another as an org, its tree with it, and its sole proprietorship lapses', () => { const acme = ev('found', null, { name: 'acme' }), ops = ev('birth', acme.id, { name: 'ops' }), bot = ev('birth', ops.id, { name: 'bot', kind: 'agent' }); const lab = ev('birth', ops.id, { name: 'lab' }), beta = ev('found', null, { name: 'beta' }), sole = ev('charter', acme.id, { form: 'sole' }); const base = [acme, ops, bot, lab, beta, sole], j = join(acme, beta, 2), a = adopt(beta, acme, 2), s = reduce([...base, j, a]); const node = e => s.nodes.get(e.id); assert.ok(s.log.every(e => e.ok), s.log.filter(e => !e.ok).map(e => e.why).join('; ')); assert.deepEqual([node(acme).kind, node(acme).parent, node(acme).root, node(acme).edge, node(acme).index], ['org', beta.id, beta.id, 'derived', 0]); assert.deepEqual([s.roots.map(r => r.name), [ops, bot, lab].map(e => node(e).root)], [['beta'], [beta.id, beta.id, beta.id]]); assert.deepEqual(rows(s, beta.id).filter(r => r.type === 'node').map(r => [r.node.name, r.path]), [['beta', '●'], ['acme', "0'"], ['ops', "0'"], ['bot', "0'"], ['lab', "0'"]]); assert.deepEqual(census(s, beta.id), { orgs: 3, agents: 1, belts: 0, decisions: 0, closed: 0 }); assert.deepEqual(standing(s, node(acme)), { ok: false, why: 'acme joined another holding, and a sole proprietor is a person' }); assert.deepEqual([summary(s.log.at(-2), s), summary(s.log.at(-1), s)], ['join acme to beta, a holding till now', 'adopt acme into beta, a holding till now']); // Its name is free for a new holding; an agent never moves alone; the old order of events lands the same tree. const t = reduce([...base, a, j, ev('found', null, { name: 'acme' }, 3), join(bot, beta, 3)]); assert.deepEqual([t.log.at(-2).ok, t.log.at(-1).why], [true, 'an agent changes hands only with the org it sits in']); assert.deepEqual(t.nodes.get(acme.id), node(acme)); }); // The lab: asks and offers on items and orgs (offers.js), and the flows around them (actions.js). Under the 'none' rule no // holding mines, so every balance moves only by what the test does. const TOPUP = 10_000 + FEE; // what one gas top-up takes from a node's money 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; }; 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('an ask is a sell order signed SINGLE|ANYONECANPAY: any node takes it, leading with a dummy of its own', () => { const { lab, acme, beta } = twoHoldings(), kit = act(lab, 'make', acme, { name: 'kit' }); const before = money(lab).now, height = lab.chain.blocks.length; const a = sell(lab, kit, 2 * SAT); assert.equal(lab.chain.blocks.length, height); // an item's ask is a signature, not a transaction assert.match(a.sig, /^[0-9a-f]{128}83$/); assert.equal(askStatus(lab, lineageState(lab), a), 'open'); assert.throws(() => sell(lab, kit, SAT), /^Error: kit is already listed at 2 tBTC\. Withdraw that ask first\.$/); assert.throws(() => sell(lab, kit, 100), /^Error: Ask at least 330 sats\.$/); assert.throws(() => take(lab, askId(a), acme), /^Error: acme is the seller\.$/); take(lab, askId(a), beta); assert.equal(lab.chain.blocks.length, height + 1); assert.equal(holder(lab, kit), 'beta'); const after = money(lab), { tx } = lab.chain.transactions.get(a.taken.txid); assert.deepEqual(after.log.at(-1), ['deal', height + 1, 'beta', 'acme', 2 * SAT]); assert.deepEqual([after.now.acme - before.acme, after.now.beta - before.beta], [2 * SAT, -2 * SAT - 2 * FEE - DUMMY]); assert.deepEqual(tx.vout.map(v => Number(v.value)), [DUMMY + 1000, 2 * SAT]); // the dummy pads the item's coin now assert.deepEqual(tx.vin.map(i => i.witness.length), [2, 1, 2]); assert.equal(tx.vin[1].witness[0], a.sig); assert.equal(askStatus(lab, lineageState(lab), a), 'done'); assert.throws(() => take(lab, askId(a), beta), /^Error: That ask was already taken\.$/); assert.throws(() => unlist(lab, askId(a)), /on the chain now/); // beta lists it again, then takes acme's lower offer instead: the coin moved, so the ask can never land. const resale = sell(lab, kit, SAT), bid = offer(lab, kit, acme, SAT / 2); accept(lab, bid.txid); assert.equal(askWhy(lab, lineageState(lab), resale), 'kit has moved since'); assert.throws(() => take(lab, askId(resale), acme), /^Error: This ask is stale: kit has moved since\.$/); unlist(lab, askId(resale)); assert.deepEqual(decodeLab(encodeLab(lab)).asks, lab.asks); const bad = JSON.parse(encodeLab(lab)); bad.asks[0].sig = `${bad.asks[0].sig.slice(0, -2)}01`; assert.throws(() => decodeLab(JSON.stringify(bad)), /Invalid ask/); }); test('an org sells cash-free on an ask: its money goes home, its item goes with it, it is frozen while listed, and its buyer adopts it at once', () => { const { lab, acme, beta } = twoHoldings(), ops = act(lab, 'birth', acme, { name: 'ops' }); send(lab, acme, ops, SAT); const kit = act(lab, 'make', ops, { name: 'kit' }), old = keyOf(lab.keys, lineageState(lab), lineageState(lab).nodes.get(ops)); const a = sell(lab, ops, 3 * SAT), listed = lab.chain.blocks.length; // Its money and its gas tank went to acme's SegWit address. Its item goes with it: the two inscriptions are one coin at // its key now, the one its ask signs, and the layout keeps who holds the item. const m1 = money(lab); assert.equal(m1.now.ops ?? 0, 0); assert.deepEqual(m1.log.filter(x => x[1] === listed), [['pay', listed, 'ops', 'acme', 7500], ['pay', listed, 'ops', 'acme', SAT - TOPUP - FEE]]); assert.deepEqual([holder(lab, kit), lab.chain.utxos.get(askId(a)).inscriptions.map(i => i.id), a.layout], ['ops', [ops, kit], [[kit, ops]]]); assert.throws(() => act(lab, 'decide', ops, { title: 'Hold the line' }), /^Error: ops is listed for sale at 3 tBTC\. Withdraw its ask first\.$/); assert.throws(() => send(lab, acme, ops, SAT), /^Error: ops is listed for sale/); assert.throws(() => act(lab, 'graduate', acme, { of: ops }), /^Error: ops is listed for sale/); assert.throws(() => take(lab, askId(a), act(lab, 'birth', beta, { name: 'bot', kind: 'agent' })), /^Error: Agents adopt only once they graduate\.$/); // beta takes it: the deal, the handshake and the handoff to its new keys, in one beat. take(lab, askId(a), beta); const s = lineageState(lab), node = s.nodes.get(ops), k = keyOf(lab.keys, s, node); assert.equal(lab.chain.blocks.length, listed + 2); // the agent's birth took one beat assert.deepEqual([node.parent, node.root, node.index, node.moves.map(x => x.from)], [beta, beta, 0, [acme]]); assert.deepEqual(s.log.slice(-2).map(e => [e.op, e.ok, summary(e, s)]), [['adopt', true, 'adopt ops into beta, from acme'], ['join', true, 'join ops to beta, from acme']]); assert.deepEqual([k.from, holderOf(lab, s, ops).who], ["beta root › 0'", 'own']); assert.notEqual(k.wallet.address, old.wallet.address); // The coin it came in split back: its inscription and its item, each in a coin of its own at its new key. const coin = id => lab.chain.coins(k.wallet.script).find(c => c.inscriptions.some(i => i.id === id)); assert.deepEqual([holder(lab, kit), coin(ops).inscriptions.length, coin(kit).inscriptions.length], ['ops', 1, 1]); assert.deepEqual(money(lab).log.filter(x => x[0] === 'deal'), [['deal', listed + 2, 'beta', 'acme', 3 * SAT]]); // It acts again as beta's, on beta's money. assert.throws(() => act(lab, 'decide', ops, { title: 'New owners' }), /^Error: ops has no test BTC for gas\. beta can send some\.$/); send(lab, beta, ops, SAT); act(lab, 'decide', ops, { title: 'New owners' }); assert.equal(lineageState(lab).nodes.get(ops).decisions[0].title, 'New owners'); }); test('an offer on an org: its parent accepts cash-free, and the buyer adopts it when it is ready', () => { const { lab, acme, beta } = twoHoldings(), ops = act(lab, 'birth', acme, { name: 'ops' }); send(lab, acme, ops, SAT); const o = offer(lab, ops, beta, 2 * SAT); assert.throws(() => offer(lab, ops, acme, SAT), /^Error: Ops is already acme's\.$/); act(lab, 'decide', ops, { title: 'Hold the line' }); // every act spends the org's inscription, so the offer goes stale assert.equal(offerWhy(lab, lineageState(lab), o), "ops's inscription has moved since"); withdraw(lab, o.txid); accept(lab, offer(lab, ops, beta, 2 * SAT).txid); assert.equal(money(lab).now.ops ?? 0, 0); assert.equal(lineageState(lab).nodes.get(ops).parent, acme); assert.throws(() => act(lab, 'decide', ops, { title: 'x' }), /^Error: ops was sold to beta, which adopts it next\.$/); assert.throws(() => send(lab, acme, ops, SAT), /^Error: ops was sold to beta/); assert.throws(() => act(lab, 'graduate', acme, { of: ops }), /^Error: ops was sold to beta/); assert.throws(() => sell(lab, ops, SAT), /^Error: Ops is held by beta\.$/); assert.throws(() => adoptBought(lab, ops, acme), /^Error: acme does not hold ops\.$/); adoptBought(lab, ops, beta); const s = lineageState(lab); assert.deepEqual([s.nodes.get(ops).parent, holderOf(lab, s, ops).who, frozenWhy(lab, s, 'decide', ops)], [beta, 'own', '']); assert.deepEqual(money(lab).log.filter(x => x[0] === 'deal').map(x => x.slice(2)), [['beta', 'acme', 2 * SAT]]); });