Orgchain v1.7: items, standing offers and the first deal; a Mainnet setting

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
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-15 00:32:29 -07:00
1 parent 32e8131500
commit 69a97544da
15 files changed
+764 -111

No files matched your search

+28 -2
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { Tx } from '@cmdcode/tapscript';
import { Signer, Tx } from '@cmdcode/tapscript';
import { TestWallet } from '../src/adapters/wallet.js';
import { secp256k1 } from '@noble/curves/secp256k1';
import { bytesToHex } from '@noble/curves/abstract/utils';
import { LocalChain } from '../src/adapters/chain.js';
@@ -47,6 +48,31 @@ test('the toy chain mines to SegWit, splits a coinbase, and moves P2WPKH spends
assert.deepEqual([copy.balance(a.script), copy.balance(b.script)], [2.5 * REWARD - SAT - FEE, SAT + REWARD / 2]);
});
test('ordinary spends take up to eight inputs and outputs, and a key-path signature may be SINGLE|ANYONECANPAY', () => {
const chain = new LocalChain(), seller = TestWallet.create(), buyer = TestWallet.create();
chain.mine(seller.script);
chain.mine(buyer.script);
const [s] = chain.coins(seller.script), [b] = chain.coins(buyer.script), vin = c => getVin(c.txid, c.vout, c.value, c.scriptPubKey);
const fan = n => { const tx = Tx.create({ vin: [vin(b)], vout: Array.from({ length: n }, (_, i) => getVout(i < n - 1 ? SAT : b.value - (n - 1) * SAT - FEE, buyer.script)) }); tx.vin[0].witness = [buyer.sign(tx)]; return Tx.encode(tx).hex; };
assert.throws(() => chain.submitPackage([fan(9)]), /Unsupported transaction shape/);
chain.submitPackage([fan(8)]);
assert.equal(chain.coins(buyer.script).length, 8);
// A sell order: the seller signs its coin and the output it wants paid, and nothing else; a buyer fills it later.
const order = Tx.create({ vin: [vin(s)], vout: [getVout(s.value - FEE, seller.script)] });
const sig = Signer.taproot.sign(seller.secret, order, 0, { sigflag: 0x83 }).hex;
assert.match(sig, /^[0-9a-f]{128}83$/);
const [c] = chain.coins(buyer.script), fill = (paid = s.value - FEE) => {
const tx = Tx.create({ vin: [vin(s), vin(c)], vout: [getVout(paid, seller.script), getVout(c.value - (paid - (s.value - FEE)), buyer.script)] });
tx.vin[0].witness = [sig];
tx.vin[1].witness = [buyer.sign(tx, undefined, 1)];
return Tx.encode(tx).hex;
};
assert.throws(() => chain.submitPackage([fill(s.value - FEE - 1000)]), /Invalid signature/); // the seller's output is fixed
chain.submitPackage([fill()]);
chain.mine(buyer.script);
assert.equal(chain.balance(seller.script), REWARD - FEE);
});
test('every act pays its own gas, and an unfunded node is stopped before anything is signed', () => {
const lab = emptyLab();
const acme = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', acme, { name: 'ops' });
@@ -135,7 +161,7 @@ test('graduation moves money to the new keys, and the graduate pays its own clai
seedSample(lab);
const { now, log, live } = money(lab);
assert.deepEqual(now, { yolopreneur: 300 * SAT - 50 * SAT - 2 * FEE - TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
lineage: 2 * SAT - FEE - TOPUP, time: 38.5 * SAT - 2 * FEE - 3 * TOPUP, sol: SAT, luna: 0.5 * SAT });
lineage: 2 * SAT - FEE - TOPUP, time: 38.5 * SAT - 2 * FEE - 3 * TOPUP, sol: SAT - TOPUP, luna: 0.5 * SAT });
assert.deepEqual(log.slice(-2), [['move', 7, 'lineage', 'lineage', 2 * SAT - FEE], ['gas', 7, 'lineage', 'lineage', 10_000]]);
const lineage = [...live.nodes.values()].find(n => n.name === 'lineage');
assert.equal(lab.chain.balance(payerOf(lab.keys, live, lineage).script), 2 * SAT - FEE - TOPUP);
+169
View File
@@ -0,0 +1,169 @@
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]);
});
+67
View File
@@ -233,3 +233,70 @@ test('the sample replays, any beat can be inspected, and the lab survives a relo
await expect(page.locator('#holdings-list li')).toHaveCount(0);
expect(problems).toEqual([]);
});
test('a node makes an item, another holding bids on it, the holder accepts; on mainnet holdings are funded', async ({ page }) => {
const problems = await open(page), inspect = page.locator('#inspect-body'), why = page.locator('#act-why');
const hold = name => page.locator('#holdings-list .hold', { has: page.locator('.hn', { hasText: new RegExp(`^${name}$`) }) });
await found(page, 'acme');
await expect(say(page)).toHaveText('acme is founded, at beat 2.');
await found(page, 'beta');
await expect(say(page)).toHaveText('beta is founded, at beat 3.');
await verb(page, 'offer');
await expect(why).toHaveText('Nobody else holds an item yet. Items come from Make.');
await hold('acme').click();
await verb(page, 'make');
await expect(page.locator('#f-make .hint')).toContainText('public and permanent');
await page.locator('#make-name').fill('Kit');
await page.locator('#make-note').fill('A starter kit.');
await submit(page, 'make');
await expect(say(page)).toHaveText('acme made kit, at beat 4.');
await expect(page.locator('#timeline-list')).toContainText('make kit · acme');
await expect(inspect).toContainText('kit · A starter kit. (made at beat 4)');
await hold('beta').click();
await verb(page, 'offer');
await expect(page.locator('#act-gas')).toHaveText("Fee: 500 sats, from beta's balance, paid only if the holder accepts.");
await page.locator('#offer-item').selectOption({ label: 'kit · held by acme' });
await page.locator('#offer-price').fill('2');
await submit(page, 'offer');
await expect(say(page)).toHaveText('beta offers 2 tBTC for kit. The offer stands until acme accepts it.');
await expect(page.locator('#clock-beat')).toHaveText('4'); // an offer is signatures, not a transaction
await expect(inspect).toContainText('2 tBTC for kit (open until acme accepts)');
await hold('acme').click();
await expect(inspect).toContainText('beta offers 2 tBTC');
await inspect.getByRole('button', { name: "Accept beta's offer for kit" }).click();
await expect(say(page)).toHaveText('acme accepted: kit goes to beta for 2 tBTC, at beat 5.');
await expect(inspect).toContainText('kit · A starter kit. (now held by beta)');
await page.locator('#timeline-list .ev', { hasText: 'kit acme → beta, for 2 tBTC' }).click();
for (const text of ['Price', '2 tBTC', "beta's SegWit key (SIGHASH_ALL) when it made the offer, then acme's Taproot key when it accepted"]) {
await expect(inspect).toContainText(text);
}
await page.locator('[data-rule="none"]').click();
await expect(say(page)).toHaveText('Mainnet: no holding mines. Beats pay the lab wallet, and you fund holdings from it.');
await expect(page.locator('#clock-pays')).toHaveText('beats pay the lab wallet');
await page.locator('#fund-to').selectOption({ label: 'beta' });
await page.locator('#fund-amount').fill('3');
await page.locator('#f-fund [type="submit"]').click();
await expect(say(page)).toHaveText('The lab wallet funded beta with 3 tBTC, at beat 6.');
await hold('beta').click();
await expect(page.locator('#timeline-list')).toContainText('3 tBTC lab wallet → beta');
await expect(page.locator('#holdings-list')).not.toContainText('of each beat');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(problems).toEqual([]);
});
test('the sample ends on a standing offer, and its holder accepts it', async ({ page }) => {
const problems = await open(page), inspect = page.locator('#inspect-body');
await page.locator('#sample').click();
await expect(say(page)).toContainText('Loaded the yolopreneur sample', { timeout: 20_000 });
await row(page, 'lineage').click();
await expect(inspect).toContainText('0.1 tBTC for launch-kit (open until sol accepts)');
await row(page, 'sol').click();
await expect(inspect).toContainText('launch-kit · The page, the post and the checklist every venture launches with.');
await inspect.getByRole('button', { name: "Accept lineage's offer for launch-kit" }).click();
await expect(say(page)).toHaveText('sol accepted: launch-kit goes to lineage for 0.1 tBTC, at beat 8.');
await expect(page.locator('#timeline-list')).toContainText('launch-kit sol → lineage, for 0.1 tBTC');
await row(page, 'lineage').click();
await expect(inspect).toContainText('launch-kit · The page, the post and the checklist every venture launches with. (made by sol at beat 6)');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(problems).toEqual([]);
});