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
92 lines
7.2 KiB
JavaScript
92 lines
7.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { Script, Tap, Tx } from '@cmdcode/tapscript';
|
|
import { LocalChain, tapscriptValid } from '../src/adapters/chain.js';
|
|
import { TestWallet } from '../src/adapters/wallet.js';
|
|
import { InscriptionAdapter, POSTAGE, FEE, envelope } from '../src/adapters/inscription.js';
|
|
import { MetadataIndex } from '../src/adapters/metadata-index.js';
|
|
import { getVin, getVout } from '../src/upstream/primitives.js';
|
|
import { census, rows } from '../src/orgchain/lineage.js';
|
|
import { ordClient, ordEvents, ordState } from '../src/indexer/ord.js';
|
|
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.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, 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 · moved",
|
|
" │ └─ 1' lineage · graduated", " └─ 1' time", ' ├─ agents', " │ ├─ 0' luna", " │ ├─ 1' sol", " │ └─ 2' astra",
|
|
' └─ 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
|
|
// graduated, and the one it published a block later stands. Four keyrings, four keys each, are bound.
|
|
assert.deepEqual(s.log.filter(e => e.op === 'keyring').map(e => s.nodes.get(e.node).name), ['yolopreneur', 'sol', 'lineage', 'lineage', 'scout']);
|
|
assert.deepEqual([...s.nodes.values()].filter(n => n.keyring).map(n => [n.name, n.keyring.retired]).sort(),
|
|
[['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 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, 13);
|
|
});
|
|
|
|
test('a reveal commits to its exact script (BIP-341), and the lab still loads reveals made the old way', () => {
|
|
const chain = new LocalChain(), w = TestWallet.create();
|
|
chain.mine(w.script);
|
|
new InscriptionAdapter(chain, w).inscribe('found x', null, { op: 'found', name: 'x' }, { metaprotocol: 'lineage/v0' });
|
|
const prevout = raw => { const out = Tx.decode(raw).vout[0]; return { value: Number(out.value), scriptPubKey: out.scriptPubKey }; };
|
|
const reveal = Tx.decode(chain.mempool[1]);
|
|
reveal.vin[0].prevout = prevout(chain.mempool[0]);
|
|
assert.equal(tapscriptValid(reveal, 0), true);
|
|
// Before v1.9 the leaf came from tapscript's encodeScript, which re-encodes the hex script; real Bitcoin rejected that.
|
|
const old = new LocalChain();
|
|
old.mine(w.script);
|
|
const [coin] = old.coins(w.script), script = envelope(w.pubkey, 'found y', null, { op: 'found', name: 'y' }, { metaprotocol: 'lineage/v0' });
|
|
const leaf = Tap.encodeScript(script), [tapkey, control] = Tap.getPubKey(w.pubkey, { target: leaf });
|
|
const commit = Tx.create({ vin: [getVin(coin.txid, coin.vout, coin.value, coin.scriptPubKey)],
|
|
vout: [getVout(POSTAGE + FEE, ['OP_1', tapkey]), getVout(coin.value - POSTAGE - 2 * FEE, w.script)] });
|
|
commit.vin[0].witness = [w.sign(commit)];
|
|
const legacy = Tx.create({ vin: [getVin(Tx.util.getTxid(commit), 0, POSTAGE + FEE, ['OP_1', tapkey])], vout: [getVout(POSTAGE, w.script)] });
|
|
legacy.vin[0].witness = [w.sign(legacy, leaf, 0), Script.encode(script, false).hex, control];
|
|
const raws = [Tx.encode(commit).hex, Tx.encode(legacy).hex], check = Tx.decode(raws[1]);
|
|
check.vin[0].prevout = prevout(raws[0]);
|
|
assert.equal(tapscriptValid(check, 0), false);
|
|
old.submitPackage(raws); // still accepted, so labs saved before v1.9 load
|
|
assert.equal(new MetadataIndex(old).events.length, 1);
|
|
});
|
|
|
|
test('a chain can start from coins imported from a real chain, and keeps them through a rebuild', () => {
|
|
const chain = new LocalChain(), w = TestWallet.create(), coin = { txid: 'ab'.repeat(32), vout: 1, value: 2e8, script: hex(w.script) };
|
|
chain.importCoin(coin);
|
|
assert.throws(() => chain.importCoin(coin), /already in this chain/);
|
|
assert.throws(() => chain.importCoin({ ...coin, vout: 2, value: 51e8 }), /Invalid imported coin/);
|
|
const id = new InscriptionAdapter(chain, w).inscribe('found x', null, { op: 'found', name: 'x' }, { metaprotocol: 'lineage/v0' });
|
|
chain.mine(NUMS);
|
|
const copy = new LocalChain(chain.snapshot());
|
|
assert.deepEqual(copy.imports, [coin]);
|
|
assert.deepEqual(copy.imported(coin.txid, 1), { value: 2e8, scriptPubKey: coin.script });
|
|
assert.deepEqual(new MetadataIndex(copy).events.map(e => [e.id, e.status]), [[id, 'confirmed']]);
|
|
assert.equal(copy.coins(w.script).length, 2); // the reveal's postage and the commit's change
|
|
});
|