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
This commit is contained in:
HyperboxandClaude Opus 5.5 committed 2026-10-15 13:32:29 -07:00
1 parent 4ad75f0970
commit adca2709f0
20 files changed
+1018 -195

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { MetadataIndex } from '../src/adapters/metadata-index.js';
import { PROTOCOL, kidsOf, lineageEvents, reduce, rows, summary } from '../src/orgchain/lineage.js';
import { decodeLab, emptyLab, encodeLab, setRule } from '../src/orgchain/store.js';
import { accept, act, adopt as adoptBought, deposit, frozenWhy, lineageState, offer, sell, send, take, unlist, withdraw } from '../src/orgchain/actions.js';
import { FEE, SAT, balances, ledger, owners, tanks } from '../src/orgchain/money.js';
import { holderOf, keyOf } from '../src/orgchain/keys.js';
import { DUMMY, askId, askStatus, askWhy, dealsOf, holderOfItem, offerWhy } from '../src/orgchain/offers.js';
let n = 0;
const ev = (op, parent = null, meta = {}, height = 1) => ({ id: `${String(++n).padStart(64, '0')}i0`, status: height === null ? 'pending' : 'confirmed',
height, metadata: { op, ...meta }, parent, protocol: PROTOCOL });
const join = (node, to, height = 2) => ev('join', node.id, { to: to.id ?? to }, height);
const adopt = (to, node, height = 2) => ev('adopt', to.id, { of: node.id ?? node }, height);
const why = (s, e) => { const entry = s.log.find(x => x.id === e.id); return entry.ok ? 'ok' : entry.why; };
test('an org changes hands by a handshake: it joins a new parent, which adopts it, in either order', () => {
const acme = ev('found', null, { name: 'acme' }), ops = ev('birth', acme.id, { name: 'ops' }), bot = ev('birth', ops.id, { name: 'bot', kind: 'agent' });
const kit = ev('birth', acme.id, { name: 'kit' }), beta = ev('found', null, { name: 'beta' }), ghost = 'f'.repeat(64) + 'i0';
const tries = [
[join(ops, beta), 'ops has derived children, and an org changes hands alone'],
[join(acme, beta), 'only orgs change hands'],
[adopt(beta, bot), 'only orgs change hands'],
[join(kit, bot), 'agents adopt only once they graduate'],
[join(kit, acme), "kit is already acme's"],
[join(kit, kit), 'kit cannot join itself or anything below it'],
[adopt(beta, ghost), 'the org must be a node in this lineage'],
[join(kit, ghost), 'the new parent must be a node in this lineage'],
];
const base = [acme, ops, bot, kit, beta], j = join(kit, beta, 3), a = adopt(beta, kit, 3);
const s = reduce([...base, ...tries.map(([e]) => e), j, a]);
tries.forEach(([e, want]) => assert.equal(why(s, e), want));
const k = s.nodes.get(kit.id), entry = id => s.log.find(x => x.id === id);
assert.deepEqual([k.parent, k.root, k.edge, k.index, k.joining], [beta.id, beta.id, 'derived', 0, null]);
assert.deepEqual(k.moves, [{ from: acme.id, index: 1, to: beta.id, beat: 3, join: j.id, adopt: a.id }]);
assert.deepEqual([s.nodes.get(acme.id).children.includes(kit.id), kidsOf(s.nodes, s.nodes.get(acme.id)).map(x => x.name)], [true, ['ops']]);
assert.deepEqual([entry(j.id).roots, entry(a.id).roots], [[acme.id, beta.id], [beta.id, acme.id]]);
assert.deepEqual([summary(entry(j.id), s), summary(entry(a.id), s)], ['join kit to beta, from acme', 'adopt kit into beta, from acme']);
// A miner may put the two halves in either order: the same tree, and the same record of each, either way.
const t = reduce([...base, ...tries.map(([e]) => e), a, j]), view = (st, id) => { const x = st.log.find(e => e.id === id); return [x.ok, x.root, x.roots, x.node]; };
assert.deepEqual(t.nodes.get(kit.id), k);
assert.deepEqual([view(t, j.id), view(t, a.id)], [view(s, j.id), view(s, a.id)]);
// Alone, a half waits for the other.
const half = reduce([...base, j]), other = reduce([...base, a]);
assert.deepEqual([summary(half.log.at(-1), half), summary(other.log.at(-1), other)], ['join kit to beta, for beta to adopt', 'adopt kit into beta, once kit joins']);
assert.deepEqual([half.nodes.get(kit.id).parent, other.nodes.get(kit.id).parent], [acme.id, acme.id]);
// acme's rows keep kit at its old index, away; beta's draw it at its new one.
assert.deepEqual(rows(s, acme.id).filter(r => r.type === 'node').map(r => [r.node.name, r.path, r.away, r.moved]),
[['acme', '●', false, false], ['ops', "0'", false, false], ['bot', "0'", false, false], ['kit', "1'", true, true]]);
assert.deepEqual(rows(s, beta.id).filter(r => r.type === 'node').map(r => [r.node.name, r.path, r.away]), [['beta', '●', false], ['kit', "0'", false]]);
// Its old index is never reused, and its name is free again under acme.
const u = reduce([...base, j, a, ev('birth', acme.id, { name: 'kit' }, 4)]), again = u.log.at(-1);
assert.deepEqual([again.ok, u.nodes.get(again.id).index], [true, 2]);
});
test('a joining org cannot graduate; a close leaves an org that moved away alone; a graduate cannot move', () => {
const acme = ev('found', null, { name: 'acme' }), kit = ev('birth', acme.id, { name: 'kit' }), beta = ev('found', null, { name: 'beta' });
const j = join(kit, beta), held = ev('graduate', acme.id, { of: kit.id }, 2), a = adopt(beta, kit, 3), shut = ev('close', acme.id, {}, 4);
const s = reduce([acme, kit, beta, j, held, a, shut]);
assert.equal(why(s, held), 'kit is joining beta');
assert.deepEqual([s.nodes.get(kit.id).status, s.log.at(-1).also], ['active', []]);
// Under beta, kit graduates into its own holding; a holding's root does not join another.
const t = reduce([acme, kit, beta, j, a, ev('graduate', beta.id, { of: kit.id }, 4), join(kit, acme, 5)]);
assert.equal(t.log.at(-2).ok, true);
assert.equal(t.log.at(-1).why, 'kit is a holding of its own, and only orgs inside a holding change hands');
// A later join replaces a pending one; an adoption waits for its own join.
const gamma = ev('found', null, { name: 'gamma' });
const u = reduce([acme, kit, beta, gamma, join(kit, beta), join(kit, gamma), adopt(beta, kit, 3)]);
assert.deepEqual([u.nodes.get(kit.id).parent, u.nodes.get(kit.id).joining.to, u.nodes.get(kit.id).moves], [acme.id, gamma.id, []]);
});
// The lab: asks and offers on items and orgs (offers.js), and the flows around them (actions.js). Under the 'none' rule no
// holding mines, so every balance moves only by what the test does.
const TOPUP = 10_000 + FEE; // what one gas top-up takes from a node's money
function money(lab) {
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;
return { now: Object.fromEntries([...balances(lab.chain, owner)].map(([id, v]) => [name(id), v])),
log: ledger(lab.chain, owner, tanks(lab.keys, events, live), dealsOf(lab)).map(m => [m.kind, m.beat, name(m.from), name(m.to), m.amount]) };
}
const holder = (lab, id) => { const events = lineageEvents(new MetadataIndex(lab.chain)); return holderOfItem(lab, events, lineageState(lab), id)?.node.name ?? null; };
function twoHoldings() {
const lab = emptyLab();
setRule(lab, 'none');
const acme = act(lab, 'found', null, { name: 'acme' }), beta = act(lab, 'found', null, { name: 'beta' });
deposit(lab, acme, 10 * SAT);
deposit(lab, beta, 10 * SAT);
return { lab, acme, beta };
}
test('an ask is a sell order signed SINGLE|ANYONECANPAY: any node takes it, leading with a dummy of its own', () => {
const { lab, acme, beta } = twoHoldings(), kit = act(lab, 'make', acme, { name: 'kit' });
const before = money(lab).now, height = lab.chain.blocks.length;
const a = sell(lab, kit, 2 * SAT);
assert.equal(lab.chain.blocks.length, height); // an item's ask is a signature, not a transaction
assert.match(a.sig, /^[0-9a-f]{128}83$/);
assert.equal(askStatus(lab, lineageState(lab), a), 'open');
assert.throws(() => sell(lab, kit, SAT), /^Error: kit is already listed at 2 tBTC\. Withdraw that ask first\.$/);
assert.throws(() => sell(lab, beta, SAT), /^Error: Only orgs change hands\.$/);
assert.throws(() => sell(lab, kit, 100), /^Error: Ask at least 330 sats\.$/);
assert.throws(() => take(lab, askId(a), acme), /^Error: acme is the seller\.$/);
take(lab, askId(a), beta);
assert.equal(lab.chain.blocks.length, height + 1);
assert.equal(holder(lab, kit), 'beta');
const after = money(lab), { tx } = lab.chain.transactions.get(a.taken.txid);
assert.deepEqual(after.log.at(-1), ['deal', height + 1, 'beta', 'acme', 2 * SAT]);
assert.deepEqual([after.now.acme - before.acme, after.now.beta - before.beta], [2 * SAT, -2 * SAT - 2 * FEE - DUMMY]);
assert.deepEqual(tx.vout.map(v => Number(v.value)), [DUMMY + 1000, 2 * SAT]); // the dummy pads the item's coin now
assert.deepEqual(tx.vin.map(i => i.witness.length), [2, 1, 2]);
assert.equal(tx.vin[1].witness[0], a.sig);
assert.equal(askStatus(lab, lineageState(lab), a), 'done');
assert.throws(() => take(lab, askId(a), beta), /^Error: That ask was already taken\.$/);
assert.throws(() => unlist(lab, askId(a)), /on the chain now/);
// beta lists it again, then takes acme's lower offer instead: the coin moved, so the ask can never land.
const resale = sell(lab, kit, SAT), bid = offer(lab, kit, acme, SAT / 2);
accept(lab, bid.txid);
assert.equal(askWhy(lab, lineageState(lab), resale), 'kit has moved since');
assert.throws(() => take(lab, askId(resale), acme), /^Error: This ask is stale: kit has moved since\.$/);
unlist(lab, askId(resale));
assert.deepEqual(decodeLab(encodeLab(lab)).asks, lab.asks);
const bad = JSON.parse(encodeLab(lab));
bad.asks[0].sig = `${bad.asks[0].sig.slice(0, -2)}01`;
assert.throws(() => decodeLab(JSON.stringify(bad)), /Invalid ask/);
});
test('an org sells cash-free on an ask: what it holds goes home, it is frozen while listed, and its buyer adopts it at once', () => {
const { lab, acme, beta } = twoHoldings(), ops = act(lab, 'birth', acme, { name: 'ops' });
send(lab, acme, ops, SAT);
const kit = act(lab, 'make', ops, { name: 'kit' }), old = keyOf(lab.keys, lineageState(lab), lineageState(lab).nodes.get(ops));
assert.throws(() => sell(lab, acme, SAT), /^Error: Only orgs change hands\.$/);
const a = sell(lab, ops, 3 * SAT), listed = lab.chain.blocks.length;
// Its money and its gas tank went to acme's SegWit address, and its item to acme's key.
const m1 = money(lab);
assert.equal(m1.now.ops ?? 0, 0);
assert.deepEqual(m1.log.filter(x => x[1] === listed), [['pay', listed, 'ops', 'acme', 7500], ['pay', listed, 'ops', 'acme', SAT - TOPUP - FEE]]);
assert.equal(holder(lab, kit), 'acme');
assert.throws(() => act(lab, 'decide', ops, { title: 'Hold the line' }), /^Error: ops is listed for sale at 3 tBTC\. Withdraw its ask first\.$/);
assert.throws(() => send(lab, acme, ops, SAT), /^Error: ops is listed for sale/);
assert.throws(() => act(lab, 'graduate', acme, { of: ops }), /^Error: ops is listed for sale/);
assert.throws(() => take(lab, askId(a), act(lab, 'birth', beta, { name: 'bot', kind: 'agent' })), /^Error: Agents adopt only once they graduate\.$/);
// beta takes it: the deal, the handshake and the handoff to its new keys, in one beat.
take(lab, askId(a), beta);
const s = lineageState(lab), node = s.nodes.get(ops), k = keyOf(lab.keys, s, node);
assert.equal(lab.chain.blocks.length, listed + 2); // the agent's birth took one beat
assert.deepEqual([node.parent, node.root, node.index, node.moves.map(x => x.from)], [beta, beta, 0, [acme]]);
assert.deepEqual(s.log.slice(-2).map(e => [e.op, e.ok, summary(e, s)]), [['adopt', true, 'adopt ops into beta, from acme'], ['join', true, 'join ops to beta, from acme']]);
assert.deepEqual([k.from, holderOf(lab, s, ops).who], ["beta root › 0'", 'own']);
assert.notEqual(k.wallet.address, old.wallet.address);
assert.deepEqual(money(lab).log.filter(x => x[0] === 'deal'), [['deal', listed + 2, 'beta', 'acme', 3 * SAT]]);
// It acts again as beta's, on beta's money.
assert.throws(() => act(lab, 'decide', ops, { title: 'New owners' }), /^Error: ops has no test BTC for gas\. beta can send some\.$/);
send(lab, beta, ops, SAT);
act(lab, 'decide', ops, { title: 'New owners' });
assert.equal(lineageState(lab).nodes.get(ops).decisions[0].title, 'New owners');
});
test('an offer on an org: its parent accepts cash-free, and the buyer adopts it when it is ready', () => {
const { lab, acme, beta } = twoHoldings(), ops = act(lab, 'birth', acme, { name: 'ops' });
send(lab, acme, ops, SAT);
const o = offer(lab, ops, beta, 2 * SAT);
assert.throws(() => offer(lab, ops, acme, SAT), /^Error: Ops is already acme's\.$/);
act(lab, 'decide', ops, { title: 'Hold the line' }); // every act spends the org's inscription, so the offer goes stale
assert.equal(offerWhy(lab, lineageState(lab), o), "ops's inscription has moved since");
withdraw(lab, o.txid);
accept(lab, offer(lab, ops, beta, 2 * SAT).txid);
assert.equal(money(lab).now.ops ?? 0, 0);
assert.equal(lineageState(lab).nodes.get(ops).parent, acme);
assert.throws(() => act(lab, 'decide', ops, { title: 'x' }), /^Error: ops was sold to beta, which adopts it next\.$/);
assert.throws(() => send(lab, acme, ops, SAT), /^Error: ops was sold to beta/);
assert.throws(() => act(lab, 'graduate', acme, { of: ops }), /^Error: ops was sold to beta/);
assert.throws(() => sell(lab, ops, SAT), /^Error: Ops is held by beta\.$/);
assert.throws(() => adoptBought(lab, ops, acme), /^Error: acme does not hold ops\.$/);
adoptBought(lab, ops, beta);
const s = lineageState(lab);
assert.deepEqual([s.nodes.get(ops).parent, holderOf(lab, s, ops).who, frozenWhy(lab, s, 'decide', ops)], [beta, 'own', '']);
assert.deepEqual(money(lab).log.filter(x => x[0] === 'deal').map(x => x.slice(2)), [['beta', 'acme', 2 * SAT]]);
});
+1 -1
View File
@@ -1 +1 @@
{"from":120,"ord":{"/blockheight":{"status":200,"body":"130"},"/inscriptions/block/120/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/121/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/122/0":{"status":200,"body":"{\"ids\":[\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\"],\"more\":false,\"page_index\":0}"},"/inscription/670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0":{"status":200,"body":"{\"address\":\"bcrt1p98e82vaue2cf0t3dh7ew6rtkvwfmfthgkpquqpvuky2upyac3a3szcnws7\",\"charms\":[],\"child_count\":4,\"children\":[\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"3323322d47592ab9977d8a3c8cbf7894581d287b045497074683046a5af74f94i0\",\"7038f2054a1019fcf4b14a099c101b55c350ae9c256bdf2700e1fa4ccfaa7b2ei0\"],\"content_length\":17,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":122,\"id\":\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"number\":0,\"parents\":[],\"previous\":null,\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"7038f2054a1019fcf4b14a099c101b55c350ae9c256bdf2700e1fa4ccfaa7b2e:0:0\",\"timestamp\":1792059012,\"value\":1000}"},"/r/metadata/670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0":{"status":200,"body":"\"a2626f7065666f756e64646e616d656b796f6c6f7072656e657572\""},"/inscriptions/block/123/0":{"status":200,"body":"{\"ids\":[\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"3323322d47592ab9977d8a3c8cbf7894581d287b045497074683046a5af74f94i0\"],\"more\":false,\"page_index\":0}"},"/inscription/9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0":{"status":200,"body":"{\"address\":\"bcrt1pfu5jhcgwcl5ulxcedvawvfwvazr6yg47c7atwdeg4pgjlaz5y6xsmnpl8p\",\"charms\":[\"vindicated\"],\"child_count\":4,\"children\":[\"4cb00b54088a273ba4898a087f89f691cb5588788a88fbf3803548cabef9df5bi0\",\"039b50131216fb534fba55eb5303879d363fa5e79d5b9301593cbfd0a8ba5cc1i0\",\"af00a5c03ae36ce58514f3913c96278d19069b80b15596234d7269ae9df5da1bi0\",\"d685b1aa792fa8374446810022893b83e81e94d751db4dfaff933ee940e87f6di0\"],\"content_length\":16,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":123,\"id\":\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"number\":1,\"parents\":[\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\"],\"previous\":\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"d685b1aa792fa8374446810022893b83e81e94d751db4dfaff933ee940e87f6d:0:0\",\"timestamp\":1792059012,\"value\":1000}"},"/r/metadata/9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0":{"status":200,"body":"\"a3626f70656269727468646e616d656a77656c6c2d6b6e6f776e646b696e64636f7267\""},"/inscription/960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0":{"status":200,"body":"{\"address\":\"bcrt1pc4kjexj5dj6v0eqssyxhvp7gharjz57hasjt08e3c8tfjrzmqc0qudw520\",\"charms\":[\"vindicated\"],\"child_count\":9,\"children\":[\"3ccebeb504beda9f8b8ce6f18486476fd6e663d645d6341dec53235b5ff5775fi0\",\"404acff09740f6ff639788419d41f08377ef8373f517d5b92a74690e68b47ffdi0\",\"7f19f0b1aaa45c08f1a0fcd85ea0201ee27335be351f985c3c8c1325212858abi0\",\"4c156019dfc5faa5b0f4bdb5693141560d7fb64c0e253bcdb45dee21093536cbi0\"],\"content_length\":10,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":123,\"id\":\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"3323322d47592ab9977d8a3c8cbf7894581d287b045497074683046a5af74f94i0\",\"number\":2,\"parents\":[\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\"],\"previous\":\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"7f54b915ca84d5ff088a457e14a8075a332e2f2df8e4e04d4e34c0016fedbb66:0:0\",\"timestamp\":1792059012,\"value\":1000}"},"/r/metadata/960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0":{"status":200,"body":"\"a3626f70656269727468646e616d656474696d65646b696e64636f7267\""},"/inscription/3323322d47592ab9977d8a3c8cbLine truncated
{"from":131,"ord":{"/blockheight":{"status":200,"body":"143"},"/inscriptions/block/131/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/132/0":{"status":200,"body":"{\"ids\":[\"9dc64a0a883bab87e1bc1008f845a4ac93bdb3935679b81e7b79727c34aaac3ai0\"],\"more\":false,\"page_index\":0}"},"/inscription/9dc64a0a883bab87e1bc1008f845a4ac93bdb3935679b81e7b79727c34aaac3ai0":{"status":200,"body":"{\"address\":\"bcrt1p0r470xzznztntm05dq8v938p7nq330nk2y5d2zllel2jdzm2v80swckzlv\",\"charms\":[],\"child_count\":4,\"children\":[\"e47a281eb5f5851d456146daa7f47ddea213b933a996612d73fd8d74689baf3di0\",\"6ab4416904ee6c7dfc4f253d87401bab6a67819b93d3b0bbb41f1ba33fd250f3i0\",\"3bf8e5e39d3bb4e2ad4e489384b159ac5bcce43239b99ecef45fc7e462fa2d17i0\",\"985a84296273e00e2d4e33b378bf96642688e9d71e037d2187c7209729ca4a4ei0\"],\"content_length\":17,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":132,\"id\":\"9dc64a0a883bab87e1bc1008f845a4ac93bdb3935679b81e7b79727c34aaac3ai0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"e47a281eb5f5851d456146daa7f47ddea213b933a996612d73fd8d74689baf3di0\",\"number\":26,\"parents\":[],\"previous\":\"1f3d2c833ab2a53819acdeabbec554ea5179fabf492c3d03ef0c35080585c353i0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"985a84296273e00e2d4e33b378bf96642688e9d71e037d2187c7209729ca4a4e:0:0\",\"timestamp\":1792095645,\"value\":1000}"},"/r/metadata/9dc64a0a883bab87e1bc1008f845a4ac93bdb3935679b81e7b79727c34aaac3ai0":{"status":200,"body":"\"a2626f7065666f756e64646e616d656b796f6c6f7072656e657572\""},"/inscriptions/block/133/0":{"status":200,"body":"{\"ids\":[\"e47a281eb5f5851d456146daa7f47ddea213b933a996612d73fd8d74689baf3di0\",\"6ab4416904ee6c7dfc4f253d87401bab6a67819b93d3b0bbb41f1ba33fd250f3i0\",\"3bf8e5e39d3bb4e2ad4e489384b159ac5bcce43239b99ecef45fc7e462fa2d17i0\"],\"more\":false,\"page_index\":0}"},"/inscription/e47a281eb5f5851d456146daa7f47ddea213b933a996612d73fd8d74689baf3di0":{"status":200,"body":"{\"address\":\"bcrt1pl86je4gdrwd9f6muqklvkx7p8fkdjc6lvdd8edw6p0n5yer9yeaqtx3ge0\",\"charms\":[\"vindicated\"],\"child_count\":4,\"children\":[\"9bfaee1d0b248cd4724078a266bf49449dd59bf824e75c48f2f56d2bd7e1e010i0\",\"93fb40e95e0f173e6fe1886bf4ef4150b2edb1554dccd3265a77614bd62b0293i0\",\"9f18ccf3f9a3e0e28babe30534eb5c095bd9ca2027342b861c7c019f2e762081i0\",\"e250fc385eaad5cf585a2f48edf37cd3ac530e9e3c447a5d4ba70c38fe740830i0\"],\"content_length\":16,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":133,\"id\":\"e47a281eb5f5851d456146daa7f47ddea213b933a996612d73fd8d74689baf3di0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"6ab4416904ee6c7dfc4f253d87401bab6a67819b93d3b0bbb41f1ba33fd250f3i0\",\"number\":27,\"parents\":[\"9dc64a0a883bab87e1bc1008f845a4ac93bdb3935679b81e7b79727c34aaac3ai0\"],\"previous\":\"9dc64a0a883bab87e1bc1008f845a4ac93bdb3935679b81e7b79727c34aaac3ai0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"e250fc385eaad5cf585a2f48edf37cd3ac530e9e3c447a5d4ba70c38fe740830:0:0\",\"timestamp\":1792095646,\"value\":1000}"},"/r/metadata/e47a281eb5f5851d456146daa7f47ddea213b933a996612d73fd8d74689baf3di0":{"status":200,"body":"\"a3626f70656269727468646e616d656a77656c6c2d6b6e6f776e646b696e64636f7267\""},"/inscription/6ab4416904ee6c7dfc4f253d87401bab6a67819b93d3b0bbb41f1ba33fd250f3i0":{"status":200,"body":"{\"address\":\"bcrt1pwmvsrjwv22kfpju3m8s2gsv38swfzunrmuvczjc5gp96l82ykccsut3vnp\",\"charms\":[\"vindicated\"],\"child_count\":9,\"children\":[\"4977a2f56a26a5e32326465c2f14258f053b2bb155e7771a9b7a7cc63b6b7711i0\",\"1b7089c227306b26dab0b680ce56a25700e2e1d154e9d3226430efc9ebe39684i0\",\"b9016558f19a9ab6d52e7b0b92a9b56db4a4c10a3c32663500ce1815f9d7d3b7i0\",\"0edfb7a6b0165c35c817dc70780396208ad1e2cbcc68ff480071b6a0b8aecebei0\"],\"content_length\":10,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":133,\"id\":\"6ab4416904ee6c7dfc4f253d87401bab6a67819b93d3b0bbb41f1ba33fd250f3i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"3bf8e5e39d3bb4e2ad4e489384b159ac5bcce43239b99ecef45fc7e462fa2d17i0\",\"number\":28,\"parents\":[\"9dc64a0a883bab87e1bc1008f845a4ac93bdb3935679b81e7b79727c34aaac3ai0\"],\"previous\":\"e47a281eb5f5851d456146daa7f47ddea213b933a996612d73fd8d74689baf3di0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"f7f62e1524cadb10ea9feb5992b9d4b2ac102a2ebddedc4bb9bbec3593ef855a:0:0\",\"timestamp\":1792095646,\"value\":1000}"},"/r/metadata/6ab4416904ee6c7dfc4f253d87401bab6a67819b93d3b0bbb41f1ba33fd250f3i0":{"status":200,"body":"\"a3626f70656269727468646e616d656474696d65646b696e64636f7267\""},"/inscription/3bf8e5e39d3bb4e2ad4e489384b159ac5bcce43239b99ecef45fc7eLine truncated
+14 -7
View File
@@ -14,21 +14,27 @@ import { compareWithLab } from '../src/indexer/compare.js';
// What ord answered after tools/regtest/rehearse.mjs, and the lab's public chain (tools/regtest/record.mjs).
const fixture = JSON.parse(readFileSync(new URL('./fixtures/regtest-rehearsal.json', import.meta.url), 'utf8'));
const replay = async url => { const hit = fixture.ord[new URL(url).pathname]; return new Response(hit?.body ?? '', { status: hit?.status ?? 404 }); };
const draw = (s, root) => rows(s, root).map(r => (r.type === 'group' ? `${r.guide}${r.label}` : `${r.guide}${r.path} ${r.node.name}${r.away ? ' · graduated' : ''}`));
const draw = (s, root) => rows(s, root).map(r => (r.type === 'group' ? `${r.guide}${r.label}` : `${r.guide}${r.path} ${r.node.name}${r.moved ? ' · moved' : r.away ? ' · graduated' : ''}`));
const NUMS = ['OP_1', '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'];
const hex = script => Script.encode(script, false).hex;
test('real ord indexed the regtest rehearsal, and the same reducer folds it into the trees the lab drew', async () => {
const events = await ordEvents(ordClient('http://ord.test', replay), { from: fixture.from });
assert.equal(events.length, 26);
assert.equal(events.length, 30);
assert.ok(events.every(e => e.protocol === 'lineage/v0' && e.parents <= 1 && /^[0-9a-f]{64}:\d+$/.test(e.location)));
const s = ordState(events), id = name => [...s.nodes.values()].find(n => n.name === name).id;
assert.ok(s.log.every(e => e.ok));
assert.deepEqual(s.roots.map(r => r.name), ['yolopreneur', 'lineage', 'acme']);
assert.deepEqual(draw(s, id('yolopreneur')), ['● yolopreneur', '└─ orgs', " ├─ 0' well-known", ' │ └─ orgs', " │ ├─ 0' mailr",
assert.deepEqual(draw(s, id('yolopreneur')), ['● yolopreneur', '└─ orgs', " ├─ 0' well-known", ' │ └─ orgs', " │ ├─ 0' mailr · moved",
" │ └─ 1' lineage · graduated", " └─ 1' time", ' ├─ agents', " │ ├─ 0' luna", " │ ├─ 1' sol", " │ └─ 2' astra",
' └─ orgs', " ├─ 0' docscope", " ├─ 1' dollhouse", " ├─ 2' doodleloop", " └─ 3' hearthframe"]);
assert.deepEqual(census(s, id('lineage')), { orgs: 0, agents: 0, belts: 0, decisions: 1, closed: 0 });
' └─ orgs', " ├─ 0' docscope · moved", " ├─ 1' dollhouse", " ├─ 2' doodleloop", " └─ 3' hearthframe"]);
// Two orgs changed hands: mailr on well-known's ask, which lineage took, and docscope on acme's offer, which time
// accepted. Each joined its buyer's tree at the buyer's next index.
assert.deepEqual(draw(s, id('lineage')), ['● lineage', '└─ orgs', " └─ 0' mailr"]);
assert.deepEqual(draw(s, id('acme')), ['● acme', '├─ agents', "│ └─ 0' scout", '└─ orgs', " └─ 0' docscope"]);
assert.deepEqual([...s.nodes.values()].flatMap(n => n.moves.map(m => [n.name, s.nodes.get(m.from).name, s.nodes.get(m.to).name])),
[['mailr', 'well-known', 'lineage'], ['docscope', 'time', 'acme']]);
assert.deepEqual(census(s, id('lineage')), { orgs: 1, agents: 0, belts: 0, decisions: 1, closed: 0 });
assert.deepEqual(s.nodes.get(id('scout')).badges.map(b => [b.skill, b.belt, s.nodes.get(b.by).name]), [['listen', 'white', 'well-known']]);
assert.deepEqual([...s.items.values()].map(i => [i.name, s.nodes.get(i.maker).name]), [['launch-kit', 'sol']]);
// Five keyrings, their proofs checked from ord's metadata (two 520-byte pushes each): lineage's first retired when it
@@ -38,10 +44,11 @@ test('real ord indexed the regtest rehearsal, and the same reducer folds it into
[['lineage', null], ['scout', null], ['sol', null], ['yolopreneur', null]]);
assert.equal(s.bound.size, 16);
// Byte for byte: the same events in the same beats, the same metadata and parents, every inscription in the same
// output (the launch kit where lineage's deal put it). Only the miner's order within blocks differs.
// output (the launch kit where scout's take of lineage's ask put it). Only the miner's order within blocks differs,
// a join and its adoption included.
const { problems, reordered } = compareWithLab({ chain: new LocalChain(fixture.chain) }, events);
assert.deepEqual(problems, []);
assert.equal(reordered, 9);
assert.equal(reordered, 13);
});
test('a reveal commits to its exact script (BIP-341), and the lab still loads reveals made the old way', () => {
+10 -5
View File
@@ -3,9 +3,9 @@ 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 { 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 { holderOfItem, offerStatus, offerWhy } from '../src/orgchain/offers.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
@@ -13,7 +13,7 @@ const TOPUP = 10_000 + FEE; // what one gas top-up takes from a node's money
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]));
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]) };
}
@@ -158,12 +158,17 @@ test('a payment can spend up to eight coins', () => {
assert.equal(most(small), 8 * SAT - FEE);
});
test('the sample ends on a standing offer: lineage bids for the launch kit sol made', () => {
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, s = lineageState(lab), name = id => s.nodes.get(id).name;
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]);
});
+84 -2
View File
@@ -248,7 +248,7 @@ test('a node makes an item, another holding bids on it, the holder accepts; on m
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 expect(why).toHaveText('Nothing to buy yet. Items come from Make, and an org changes hands from another parent.');
await hold('acme').click();
await verb(page, 'make');
await expect(page.locator('#f-make .hint')).toContainText('public and permanent');
@@ -290,7 +290,7 @@ test('a node makes an item, another holding bids on it, the holder accepts; on m
expect(problems).toEqual([]);
});
test('the sample ends on a standing offer, and its holder accepts it', async ({ page }) => {
test('the sample ends on both sides of a deal: sol accepts an offer, and lineage takes an ask for an org', 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 });
@@ -303,10 +303,92 @@ test('the sample ends on a standing offer, and its holder accepts it', async ({
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)');
await expect(row(page, 'mailr')).toContainText('for sale');
await verb(page, 'offer');
await page.locator('#offer-item').selectOption({ label: 'mailr · an org of well-known · asks 1 tBTC' });
await expect(page.locator('#offer-ask-line')).toHaveText("well-known asks 1 tBTC for mailr. Taking it costs the price, two 500-sat fees and a 600-sat coin of lineage's that leads the deal, and gas to adopt mailr at once.");
await page.locator('#offer-take').click();
await expect(say(page)).toHaveText("lineage bought mailr from well-known for 1 tBTC and adopted it, at beat 9: its keys derive from lineage's seed now.");
await expect(page.locator('#tree-sub')).toHaveText('lineage · now');
await expect(inspect).toContainText('from well-known to lineage, beat 9');
await expect(inspect).toContainText('derived: lineage holds its seed');
await expect(page.locator('#timeline-list')).toContainText('join mailr to lineage, from well-known');
await page.locator('#timeline-list .ev', { hasText: 'mailr yolopreneur / well-known → lineage, for 1 tBTC' }).click();
await expect(inspect).toContainText("the lot's Taproot key (SIGHASH_SINGLE|ANYONECANPAY) when well-known listed it, then lineage's SegWit key (SIGHASH_ALL) when it took it");
await page.locator('#holdings-list .hold', { has: page.locator('.hn', { hasText: /^yolopreneur$/ }) }).click();
await expect(row(page, 'mailr')).toContainText('to lineage');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(problems).toEqual([]);
});
test('an org sells cash-free: its parent lists it, it is frozen, and another holding takes it and adopts it', 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 hold('acme').click();
await birth(page, 'ops');
await expect(say(page)).toHaveText('ops is born to acme, at beat 4.');
await verb(page, 'sell');
await page.locator('#sell-lot').selectOption({ label: "ops · org 0'" });
await expect(page.locator('#sell-note')).toContainText("Cash-free: ops's money, gas tank and items go home to acme first");
await expect(page.locator('#act-key')).toContainText('Signs as ops:');
await page.locator('#sell-price').fill('3');
await submit(page, 'sell');
await expect(say(page)).toHaveText("acme lists ops at 3 tBTC: any node can take it. Whatever it held went home to acme first, and it's frozen until the ask is taken or withdrawn.");
await expect(inspect).toContainText('3 tBTC for ops (open: any node can take it)');
await row(page, 'ops').click();
await expect(inspect).toContainText('3 tBTC, asked by acme');
await verb(page, 'decide');
await expect(why).toHaveText('ops is listed for sale at 3 tBTC. Withdraw its ask first.');
await hold('beta').click();
await verb(page, 'offer');
await page.locator('#offer-item').selectOption({ label: 'ops · an org of acme · asks 3 tBTC' });
await page.locator('#offer-take').click();
await expect(say(page)).toHaveText("beta bought ops from acme for 3 tBTC and adopted it, at beat 5: its keys derive from beta's seed now.");
await expect(row(page, 'ops')).toBeVisible();
await expect(inspect).toContainText('from acme to beta, beat 5');
await expect(inspect).toContainText("beta root › 0'");
await hold('acme').click();
await expect(row(page, 'ops')).toContainText('to beta');
await expect(page.locator('#timeline-list')).toContainText('ops acme → beta, for 3 tBTC');
expect(problems).toEqual([]);
});
test('an offer on an org: its parent accepts it cash-free, and the buyer adopts it from Inspect', async ({ page }) => {
const problems = await open(page), inspect = page.locator('#inspect-body');
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 hold('acme').click();
await birth(page, 'ops');
await expect(say(page)).toHaveText('ops is born to acme, at beat 4.');
await hold('beta').click();
await verb(page, 'offer');
await page.locator('#offer-item').selectOption({ label: 'ops · an org of acme' });
await expect(page.locator('#offer-ask')).toBeHidden();
await page.locator('#offer-price').fill('2');
await submit(page, 'offer');
await expect(say(page)).toHaveText('beta offers 2 tBTC for ops. The offer stands until acme accepts it.');
await hold('acme').click();
await expect(inspect).toContainText('ops beta offers 2 tBTC');
await inspect.getByRole('button', { name: "Accept beta's offer for ops" }).click();
await expect(say(page)).toHaveText('acme accepted: ops goes to beta for 2 tBTC, at beat 5. Its money and items went home to acme first, and beta adopts it next.');
await expect(row(page, 'ops')).toContainText('sold');
await row(page, 'ops').click();
await expect(inspect).toContainText('to beta, which adopts it next');
await verb(page, 'decide');
await expect(page.locator('#act-why')).toHaveText('ops was sold to beta, which adopts it next.');
await inspect.getByRole('button', { name: 'Adopt into beta' }).click();
await expect(say(page)).toHaveText("beta adopted ops, at beat 6: its keys derive from beta's seed now.");
await expect(row(page, 'ops')).toContainText('to beta');
expect(problems).toEqual([]);
});
test('an agent pays a registry in another holding, and the registry awards it a belt', async ({ page }) => {
const problems = await open(page);
const hold = name => page.locator('#holdings-list .hold', { has: page.locator('.hn', { hasText: new RegExp(`^${name}$`) }) });