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
126 lines
7.5 KiB
JavaScript
126 lines
7.5 KiB
JavaScript
// Rehearse the yolopreneur sample on real Bitcoin Core (regtest). The lab's own action code signs every transaction on
|
|
// a LocalChain whose only money is coins imported from regtest, so nothing it makes spends a local coinbase. After each
|
|
// beat, that beat's transactions go to bitcoind byte for byte and are mined into a block; ord then indexes them like
|
|
// any other inscriptions, and bin/lineage-index.mjs folds them back into trees. Test coins on a private chain only.
|
|
// Usage: node tools/regtest/rehearse.mjs [out.json] (tools/regtest/README.md starts bitcoind and ord)
|
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import { dirname } from 'node:path';
|
|
import { Script, Tx } from '@cmdcode/tapscript';
|
|
import { TestWallet } from '../../src/adapters/wallet.js';
|
|
import { MetadataIndex } from '../../src/adapters/metadata-index.js';
|
|
import { emptyLab, encodeLab, setRule, setTempo } from '../../src/orgchain/store.js';
|
|
import { accept, act, adopt, deposit, offer, sell, send, take } from '../../src/orgchain/actions.js';
|
|
import { askId } from '../../src/orgchain/offers.js';
|
|
import { SAT } from '../../src/orgchain/money.js';
|
|
import { lineageEvents, reduce, rows } from '../../src/orgchain/lineage.js';
|
|
import { bitcoind } from './rpc.mjs';
|
|
|
|
const out = process.argv[2] ?? 'tools/regtest/out/lab.json';
|
|
const NUMS = ['OP_1', '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0']; // BIP-341's key that no one holds
|
|
const hex = script => Script.encode(script, false).hex;
|
|
const node = bitcoind(), funder = bitcoind({ wallet: 'funder' });
|
|
|
|
const lab = emptyLab();
|
|
lab.wallet = TestWallet.create();
|
|
setRule(lab, 'none'); // no holding mines: the only money is what regtest sends in
|
|
setTempo(lab, 'manual'); // the rehearsal decides when a beat lands
|
|
// Local beats pay the unspendable key, so no local coinbase is ever spent; anything else beating is a bug here.
|
|
const mineTo = lab.chain.mineTo.bind(lab.chain);
|
|
lab.chain.mineTo = vout => {
|
|
if (vout.some(o => hex(o.scriptPubKey) !== hex(NUMS))) throw new Error('Out of lab money: only the rehearsal beats, and only to the unspendable key.');
|
|
return mineTo(vout);
|
|
};
|
|
|
|
const miner = await funder('getnewaddress', 'miner', 'bech32m');
|
|
const mine = n => node('generatetoaddress', n, miner);
|
|
const height = await node('getblockcount');
|
|
if (height < 120) await mine(120 - height); // mature coins for the funder, and past ord's regtest jubilee
|
|
const funding = [];
|
|
for (const btc of [20, 20, 20, 10]) funding.push(await funder('sendtoaddress', lab.wallet.address, btc));
|
|
await mine(1);
|
|
for (const txid of funding) {
|
|
const o = (await node('getrawtransaction', txid, true)).vout.find(v => v.scriptPubKey.hex === hex(lab.wallet.script));
|
|
lab.chain.importCoin({ txid, vout: o.n, value: Math.round(o.value * SAT), script: o.scriptPubKey.hex });
|
|
}
|
|
console.log(`lab wallet ${lab.wallet.address} holds ${funding.length} coins from regtest`);
|
|
|
|
let beats = 0, sent = 0;
|
|
async function beat(label) {
|
|
lab.chain.mine(NUMS);
|
|
const raws = lab.chain.blocks.at(-1).raws.slice(1);
|
|
for (const raw of raws) {
|
|
try { await node('sendrawtransaction', raw, 0); }
|
|
catch (error) {
|
|
if (!/cluster|too-long-mempool-chain/i.test(error.message)) throw new Error(`beat ${beats + 1} (${label}), ${Tx.util.getTxid(raw)}: ${error.message}`);
|
|
await mine(1); // a long chain of cued spends: let regtest confirm what it has, then carry on
|
|
await node('sendrawtransaction', raw, 0);
|
|
}
|
|
}
|
|
const [hash] = await mine(1), block = await node('getblock', hash);
|
|
for (const raw of raws) {
|
|
if (!(await node('getrawtransaction', Tx.util.getTxid(raw), true)).confirmations) throw new Error(`${Tx.util.getTxid(raw)} did not confirm.`);
|
|
}
|
|
beats++; sent += raws.length;
|
|
console.log(`beat ${beats}: ${label} · ${raws.length} transactions → regtest block ${block.height}`);
|
|
}
|
|
|
|
const yolo = act(lab, 'found', null, { name: 'yolopreneur' });
|
|
deposit(lab, yolo, 30 * SAT);
|
|
deposit(lab, yolo, 30 * SAT);
|
|
await beat('found yolopreneur and fund it');
|
|
const wk = act(lab, 'birth', yolo, { name: 'well-known', kind: 'org' }), time = act(lab, 'birth', yolo, { name: 'time', kind: 'org' });
|
|
act(lab, 'keyring', yolo, {});
|
|
await beat("well-known and time, and yolopreneur's keyring");
|
|
send(lab, yolo, time, 40 * SAT);
|
|
send(lab, yolo, wk, 10 * SAT);
|
|
const mailr = act(lab, 'birth', wk, { name: 'mailr', kind: 'org' });
|
|
const lineage = act(lab, 'birth', wk, { name: 'lineage', kind: 'org' });
|
|
const luna = act(lab, 'birth', time, { name: 'luna', kind: 'agent' }), sol = act(lab, 'birth', time, { name: 'sol', kind: 'agent' });
|
|
act(lab, 'birth', time, { name: 'astra', kind: 'agent' });
|
|
await beat('two sends and five births');
|
|
send(lab, wk, lineage, 2 * SAT);
|
|
const [docscope] = ['docscope', 'dollhouse', 'doodleloop', 'hearthframe'].map(name => act(lab, 'birth', time, { name, kind: 'org' }));
|
|
await beat("time's ventures");
|
|
send(lab, time, sol, SAT);
|
|
send(lab, time, luna, 0.5 * SAT);
|
|
act(lab, 'award', time, { to: sol, skill: 'launch', belt: 'green' });
|
|
act(lab, 'award', time, { to: luna, skill: 'post', belt: 'white' });
|
|
act(lab, 'decide', yolo, { title: 'Every venture gets a page', why: '<venture>.yolopreneur.lol, served from one pages branch.' });
|
|
const kit = act(lab, 'make', sol, { name: 'launch-kit', note: 'The page, the post and the checklist every venture launches with.' });
|
|
act(lab, 'keyring', sol, {});
|
|
act(lab, 'keyring', lineage, {}); // retired at the next beat, when lineage graduates and its keys change
|
|
await beat('belts, a decision, an item and two keyrings');
|
|
act(lab, 'graduate', wk, { of: lineage });
|
|
act(lab, 'decide', lineage, { title: 'Lineage gets its own org', why: 'Its own repo on Teahub now, lineage.llc later: adopted, not derived.' });
|
|
accept(lab, offer(lab, kit, lineage, 0.1 * SAT).txid);
|
|
await beat('lineage graduates and buys the launch kit');
|
|
const acme = act(lab, 'found', null, { name: 'acme' });
|
|
deposit(lab, acme, 5 * SAT);
|
|
act(lab, 'keyring', lineage, {}); // its new keys, a beat after the graduation
|
|
await beat("another holding, and lineage's new keyring");
|
|
const scout = act(lab, 'birth', acme, { name: 'scout', kind: 'agent' });
|
|
send(lab, acme, scout, SAT);
|
|
await beat('an agent of acme');
|
|
send(lab, scout, wk, 0.1 * SAT);
|
|
act(lab, 'award', wk, { to: scout, skill: 'listen', belt: 'white' });
|
|
act(lab, 'keyring', scout, {});
|
|
await beat("scout pays well-known, which awards it a belt, and scout's keyring");
|
|
// Both sides of a deal (v1.12). An ask: well-known signs mailr's input SIGHASH_SINGLE|ANYONECANPAY, and lineage takes
|
|
// it behind a dummy of its own, then adopts mailr, which joins it and moves to its new keys. An offer: acme bids for
|
|
// docscope, time accepts, and acme adopts it a beat later, while scout takes lineage's ask for the launch kit.
|
|
take(lab, askId(sell(lab, mailr, SAT)), lineage);
|
|
await beat('lineage takes the ask for mailr and adopts it');
|
|
accept(lab, offer(lab, docscope, acme, 0.5 * SAT).txid);
|
|
await beat('acme buys docscope on an offer time accepts');
|
|
adopt(lab, docscope, acme);
|
|
take(lab, askId(sell(lab, kit, 0.2 * SAT)), scout);
|
|
await beat('acme adopts docscope, and scout takes the ask for the launch kit');
|
|
|
|
const live = reduce(lineageEvents(new MetadataIndex(lab.chain)), { pending: true });
|
|
for (const root of live.roots) {
|
|
console.log(rows(live, root.id).map(r => (r.type === 'group' ? `${r.guide}${r.label}` : `${r.guide}${r.path} ${r.node.name}${r.moved ? ' · moved' : r.away ? ' · graduated' : ''}`)).join('\n'));
|
|
}
|
|
mkdirSync(dirname(out), { recursive: true });
|
|
writeFileSync(out, encodeLab(lab));
|
|
console.log(`${beats} beats, ${sent} transactions confirmed on regtest (height ${await node('getblockcount')}); the lab is saved to ${out}`);
|