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, withdraw } from '../src/orgchain/actions.js'; import { FEE, REWARD, SAT, balances, ledger, most, owners, payerOf, tanks } from '../src/orgchain/money.js'; import { 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 = new Map((lab.offers ?? []).map(o => [o.txid, o])); 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 eight 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.equal(most(small), 8 * SAT - FEE); }); test('the sample ends on a standing offer: lineage bids for the launch kit sol made', () => { const lab = emptyLab(); seedSample(lab); const [o] = lab.offers, 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']); 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]); });