Files
orgchain/tests/offers.test.js
T
HyperboxandClaude Opus 5.5 adca2709f0 Orgchain v1.12: both sides of a deal, and orgs change hands
Deals go both ways now, OpenOrdex's partially signed transaction from either
side, on a lot: an item, or an org inside a holding, which its parent sells.
Buy makes an offer, the buy order: the buyer signs the whole deal and the
holder countersigns. Sell makes an ask, the sell order: the lot's key signs
its input alone with SIGHASH_SINGLE|ANYONECANPAY, committing to that input
and to the price to the seller and to nothing else, and any node can take
it, leading with a 600-sat coin of its own so the lot lands in its output.

An org sells cash-free: before its parent lists it or accepts an offer on
it, the org's money, gas tank and items go home, so the price is for the org
alone, and a listed org is frozen until its ask is taken or withdrawn. Its
buyer adopts it with two new lineage/v0 ops, in either order: join, signed
by whoever holds the org's inscription, and adopt, signed by the new parent.
The org takes the buyer's next index and derives its keys there, and its
inscription moves to its new key. Taking an ask adopts at once; after an
accepted offer the buyer adopts when it's ready. The seller's tree keeps a
row at the old index.

Rehearsed on Bitcoin Core regtest: an org sold on an ask, another on an
offer and an item on an ask, twelve beats and 104 transactions, and ord tells
the same story. The rehearsal caught that a miner can put a join and its
adoption in either order within a block, so a move records both halves and
each half belongs to both holdings. The sample ends with both sides
standing: lineage's offer for the launch kit, and well-known's ask for mailr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
2026-10-15 13:32:29 -07:00

175 lines
10 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, take, withdraw } from '../src/orgchain/actions.js';
import { FEE, REWARD, SAT, balances, ledger, most, owners, payerOf, tanks } from '../src/orgchain/money.js';
import { askId, askStatus, dealsOf, 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 = dealsOf(lab);
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 both sides of a deal: lineage bids for the launch kit, and well-known asks for mailr', () => {
const lab = emptyLab();
seedSample(lab);
const [o] = lab.offers, [a] = lab.asks, 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']);
assert.deepEqual([name(a.lot), name(a.seller), a.price, askStatus(lab, s, a)], ['mailr', 'well-known', 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]);
take(lab, askId(a), o.buyer);
const t = lineageState(lab), mailr = t.nodes.get(a.lot);
assert.deepEqual([name(mailr.parent), mailr.root, mailr.index, mailr.moves.length], ['lineage', o.buyer, 0, 1]);
assert.deepEqual(money(lab).log.filter(m => m[0] === 'deal').at(-1), ['deal', 9, 'lineage', 'well-known', SAT]);
});