The chain takes ordinary spends of up to eight inputs and outputs, whose key-path signatures may carry SIGHASH_SINGLE|ANYONECANPAY (0x83); reveals keep their two-in, two-out shape. A new make op records an item, an inscription its maker holds. Any node can offer test BTC for an item another node holds, in any holding: the buyer's SegWit key signs the whole deal, the offer stands in the lab, and the holder accepts by countersigning the item's input, so the item and the payment swap in one transaction. Offers go stale when a promised coin moves or a side re-keys or closes, and payments spend unpromised coins first. Sends take up to eight coins. Mainnet is a third mining setting: no holding mines, beats pay the lab wallet, and holdings are funded from it. The sample ends on lineage's open offer for sol's launch-kit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
170 lines
9.8 KiB
JavaScript
170 lines
9.8 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, 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]);
|
|
});
|