// Rehearse the yolopreneur sample on real Bitcoin Core (regtest), with its deals and the trees that change hands. 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, 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' })); act(lab, 'award', time, { to: sol, skill: 'launch', belt: 'white' }); // green next beat, re-inscribed over it on sol's sat await beat("time's ventures, and sol's white belt"); 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: '.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'); // Whole trees (v1.13). An offer: acme bids for time with its six below it, every coin they sit in as an input, and // yolopreneur accepts cash-free; acme adopts it a beat later, and the coin it bought splits back into the tree at // acme's keys. An ask: acme's owner lists acme whole, its tree bundled into one coin (eleven inscriptions, the launch // kit scout holds among them), and lineage takes it, adopts it, and splits it back at lineage's keys. deposit(lab, acme, 10 * SAT); accept(lab, offer(lab, time, acme, SAT).txid); await beat('acme buys time with its tree, on an offer yolopreneur accepts'); adopt(lab, time, acme); await beat("acme adopts time, and the tree splits back at acme's keys"); deposit(lab, lineage, 5 * SAT); const whole = sell(lab, acme, 2 * SAT); await beat("acme's owner lists acme whole, its tree in one coin"); take(lab, askId(whole), lineage); await beat('lineage takes acme whole and adopts it'); 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}`);