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
175 lines
10 KiB
JavaScript
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]);
|
|
});
|