A new lineage/v0 op, charter, records a node's legal form: sole proprietor, LLC, series LLC, series or corporation, with a jurisdiction (ISO 3166), a legal name, who formed it and a SHA-256 of the filings. Legal form is an attribute, so a node keeps its inscription, keys and history whatever form it takes, and the newest charter counts. A node charters itself; a series is chartered by its master, an ancestor chartered as a series LLC. Whether a charter stands is derived from the tree, never stored: a series lapses if it graduates or its master closes or stops holding series, so charters commute with graduations and closes in any block order. Agents can graduate: a seat goes independent as a holding of its own, with its own root and keys and its money along, and can then birth, award and incorporate. Charter is the tenth verb, and the tree tags each node with its form. Charter hashes a filing from a file in the tab. The sample charters yolopreneur as a Wyoming series LLC with its ventures as series, and lineage graduates into Lineage LLC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
182 lines
12 KiB
JavaScript
182 lines
12 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
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';
|
|
import { getVin, getVout } from '../src/upstream/primitives.js';
|
|
import { MetadataIndex } from '../src/adapters/metadata-index.js';
|
|
import { lineageEvents, reduce } from '../src/orgchain/lineage.js';
|
|
import { decodeLab, emptyLab, encodeLab, miningPreference, setRigs, setRule, setTempo, tick } from '../src/orgchain/store.js';
|
|
import { act, gasCheck, lastHolding, lineageState, minerOf, seedSample, send } from '../src/orgchain/actions.js';
|
|
import { FEE, PayWallet, REWARD, SAT, balances, brief, btc, ledger, most, owners, payerOf, satsOf, tankOf, tanks } from '../src/orgchain/money.js';
|
|
import { keyOf } from '../src/orgchain/keys.js';
|
|
|
|
const TOPUP = 10_000 + FEE; // what one gas top-up takes from a node's money
|
|
const key = byte => { const secret = byte.repeat(32); return new PayWallet(secret, bytesToHex(secp256k1.getPublicKey(secret, true)), "m/84'/1'/0'/0/0"); };
|
|
// A lab's money as the app sees it: balances now (or at a beat), 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;
|
|
return { 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)).map(m => [m.kind, m.beat, name(m.from), name(m.to), m.amount]) };
|
|
}
|
|
|
|
test('the toy chain mines to SegWit, splits a coinbase, and moves P2WPKH spends signed by the key they pay', () => {
|
|
const chain = new LocalChain(), a = key('11'), b = key('22');
|
|
chain.mine(a.script);
|
|
const [coin] = chain.coins(a.script);
|
|
assert.equal(coin.value, REWARD);
|
|
const build = (amount = SAT) => Tx.create({ vin: [getVin(coin.txid, coin.vout, coin.value, coin.scriptPubKey)],
|
|
vout: [getVout(amount, b.script), getVout(coin.value - amount - FEE, a.script)] });
|
|
const signed = (signer, tx = build(), witness = null) => { tx.vin[0].witness = witness ?? signer.sign(tx, 0); return Tx.encode(tx).hex; };
|
|
const good = build();
|
|
const sig = a.sign(good, 0);
|
|
assert.throws(() => chain.submitPackage([signed(b)]), /P2WPKH spends need a SIGHASH_ALL signature by the key they pay/);
|
|
assert.throws(() => chain.submitPackage([signed(a, build(), [`${sig[0].slice(0, -2)}83`, sig[1]])]), /P2WPKH spends need/);
|
|
assert.throws(() => chain.submitPackage([signed(a, build(), ['ab'.repeat(64)])]), /Unsupported witness/);
|
|
assert.throws(() => chain.submitPackage([signed(a, build(SAT + 1), sig)]), /Invalid signature/);
|
|
chain.submitPackage([signed(a, good, sig)]);
|
|
chain.mine(a.script);
|
|
assert.equal(chain.balance(b.script), SAT);
|
|
assert.equal(chain.balance(a.script), 2 * REWARD - SAT - FEE);
|
|
chain.mineTo([getVout(REWARD / 2, a.script), getVout(REWARD / 2, b.script)]);
|
|
assert.throws(() => chain.mineTo([getVout(REWARD, a.script), getVout(1000, b.script)]), /Invalid local coinbase/);
|
|
assert.equal(chain.blocks.length, 3);
|
|
const copy = new LocalChain(chain.snapshot());
|
|
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' });
|
|
const before = encodeLab(lab);
|
|
assert.throws(() => act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }), /^Error: ops has no test BTC for gas\. acme can send some\.$/);
|
|
assert.throws(() => act(lab, 'decide', ops, { title: 'Ship it' }), /ops has no test BTC for gas/);
|
|
assert.equal(encodeLab(lab), before);
|
|
send(lab, acme, ops, 12.5 * SAT);
|
|
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' });
|
|
send(lab, ops, bot, SAT);
|
|
const { now, log } = money(lab);
|
|
assert.deepEqual(now, { acme: 5 * REWARD - 12.5 * SAT - TOPUP - FEE, ops: 11.5 * SAT - TOPUP - FEE, bot: SAT });
|
|
assert.deepEqual(log.filter(([kind]) => kind !== 'mine'), [['gas', 3, 'acme', 'acme', 10_000], ['pay', 4, 'acme', 'ops', 12.5 * SAT],
|
|
['gas', 5, 'ops', 'ops', 10_000], ['pay', 6, 'ops', 'bot', SAT]]);
|
|
const s = lineageState(lab), tank = name => tankOf(lab.chain, keyOf(lab.keys, s, [...s.nodes.values()].find(n => n.name === name)).wallet);
|
|
assert.deepEqual([tank('acme'), tank('ops')], [7830, 7500]); // a birth burns three fees and hands its postage to the child
|
|
assert.equal(gasCheck(lab, s, 'decide', bot, {}), '');
|
|
const opsKey = payerOf(lab.keys, s, s.nodes.get(ops));
|
|
assert.equal(most(lab.chain.coins(opsKey.script)), 11.5 * SAT - TOPUP - 2 * FEE);
|
|
assert.throws(() => send(lab, bot, ops, 2 * SAT), /^Error: bot has 1 tBTC, and sending 2 tBTC also costs the 500-sat fee\.$/);
|
|
assert.throws(() => send(lab, ops, bot, 11.5 * SAT - TOPUP - 2 * FEE - 100),
|
|
/^Error: Sending 11\.499884 tBTC would leave 100 sats of change, under the 330-sat floor\. Send 11\.499885 tBTC, or at least 230 sats less\.$/);
|
|
});
|
|
|
|
test('beats split across holdings by hashrate, or go whole to the last act', () => {
|
|
const lab = emptyLab();
|
|
const acme = act(lab, 'found', null, { name: 'acme' }), beta = act(lab, 'found', null, { name: 'beta' });
|
|
const mined = beat => money(lab).log.filter(([kind, b]) => kind === 'mine' && b === beat).map(([, , , to, amount]) => [to, amount]);
|
|
assert.deepEqual(mined(2), [['acme', REWARD]]);
|
|
assert.deepEqual(mined(3), [['acme', REWARD / 2], ['beta', REWARD / 2]]); // beta's founding beat already splits
|
|
setRigs(lab, acme, 3);
|
|
act(lab, 'decide', beta, { title: 'Mine more' });
|
|
assert.deepEqual(mined(4), [['acme', 37.5 * SAT], ['beta', 12.5 * SAT]]);
|
|
setRigs(lab, beta, 0);
|
|
act(lab, 'decide', acme, { title: 'Alone' });
|
|
assert.deepEqual(mined(5), [['acme', REWARD]]);
|
|
setRule(lab, 'last');
|
|
setRigs(lab, beta, 1);
|
|
act(lab, 'decide', beta, { title: 'Clicked' });
|
|
assert.deepEqual(mined(6), [['beta', REWARD]]);
|
|
assert.equal(minerOf(lab, acme).length, 1);
|
|
setTempo(lab, 30, 1000);
|
|
act(lab, 'decide', acme, { title: 'First' }, 2000);
|
|
act(lab, 'decide', beta, { title: 'Last in' }, 3000);
|
|
assert.equal(lastHolding(lab), beta);
|
|
assert.equal(tick(lab, 32000, minerOf(lab, lastHolding(lab))), true);
|
|
assert.deepEqual(mined(7), [['beta', REWARD]]); // the race for a timed beat: whoever acted last takes it
|
|
assert.equal(lastHolding(lab), null);
|
|
setTempo(lab, 'each');
|
|
assert.deepEqual(decodeLab(encodeLab(lab)).mining, { rule: 'last', rigs: { [acme]: 3 } });
|
|
assert.throws(() => miningPreference({ rule: 'luck' }), /Invalid mining rule/);
|
|
assert.throws(() => miningPreference({ rigs: { [acme]: 10 } }), /Invalid hashrate/);
|
|
const three = emptyLab();
|
|
for (const name of ['a', 'b', 'c']) act(three, 'found', null, { name });
|
|
assert.deepEqual(money(three).log.filter(([kind, b]) => kind === 'mine' && b === 4).map(m => m[4]), [1666666666, 1666666666, 1666666668]);
|
|
});
|
|
|
|
test('sends cross holdings and cue at a timed tempo; closed nodes neither send nor receive', () => {
|
|
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, beta, 2 * SAT);
|
|
assert.deepEqual(money(lab).log.at(-1), ['pay', 5, 'acme', 'beta', 2 * SAT]);
|
|
setTempo(lab, 10, 1000);
|
|
send(lab, beta, ops, SAT, 2000);
|
|
assert.deepEqual(lab.clock, { tempo: 10, nextAt: 12000 });
|
|
assert.deepEqual(money(lab).log.at(-1), ['pay', null, 'beta', 'ops', SAT]);
|
|
assert.equal(money(lab).now.ops, SAT); // now includes what is cued
|
|
assert.equal(money(lab, 5).now.ops, undefined);
|
|
assert.equal(tick(lab, 12000, minerOf(lab, acme)), true);
|
|
assert.deepEqual(money(lab).log.slice(-3).map(m => m.slice(0, 4)), [['mine', 6, null, 'acme'], ['mine', 6, null, 'beta'], ['pay', 6, 'beta', 'ops']]);
|
|
assert.equal(tick(lab, 99999, minerOf(lab, acme)), false); // nothing cued: no empty beats
|
|
setTempo(lab, 'each');
|
|
act(lab, 'close', ops, {});
|
|
assert.throws(() => send(lab, acme, ops, SAT), /ops is closed/);
|
|
assert.throws(() => send(lab, ops, beta, 1000), /ops is closed/);
|
|
assert.throws(() => send(lab, acme, acme, SAT), /cannot send to itself/);
|
|
assert.throws(() => send(lab, acme, beta, 51 * SAT), /at most 50 test BTC/);
|
|
assert.throws(() => send(lab, acme, beta, 100), /at least 330 sats/);
|
|
act(lab, 'close', beta, {});
|
|
assert.equal(minerOf(lab, acme).length, 1); // a closed holding stops mining
|
|
});
|
|
|
|
test('graduation moves money to the new keys, and the graduate pays its own claim; the sample shows it', () => {
|
|
const lab = emptyLab();
|
|
seedSample(lab);
|
|
const { now, log, live } = money(lab);
|
|
// lineage graduates at beat 7 into a holding of its own, so it mines half of that beat. Its keyring at beat 6 filled
|
|
// its old gas tank, which stays with its old keys; its acts after graduating fill a new one.
|
|
assert.deepEqual(now, { yolopreneur: 275 * SAT - 50 * SAT - 2 * FEE - 2 * TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
|
|
lineage: 27 * SAT - FEE - 2 * 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 - TOPUP - 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), 27 * SAT - FEE - 2 * TOPUP);
|
|
assert.equal(money(lab, 5).now.lineage, 2 * SAT); // before graduating, at its derived address
|
|
const solo = emptyLab();
|
|
const root = act(solo, 'found', null, { name: 'acme' }), kid = act(solo, 'birth', root, { name: 'kid' });
|
|
assert.throws(() => act(solo, 'graduate', root, { of: kid }), /^Error: kid needs test BTC of its own to claim its new root\. Send it some first\.$/);
|
|
});
|
|
|
|
test('amounts read and print as test BTC', () => {
|
|
assert.deepEqual(['1.5', '0.00000001', '.5', '7', ' 2.25 ', '1.123456789', '', 'abc', '1e3', '-1'].map(satsOf),
|
|
[1.5 * SAT, 1, 0.5 * SAT, 7 * SAT, 2.25 * SAT, null, null, null, null, null]);
|
|
assert.deepEqual([4999999500, 0, REWARD, 1].map(btc), ['49.999995 tBTC', '0 tBTC', '50 tBTC', '0.00000001 tBTC']);
|
|
assert.deepEqual([4999999500, 1250000000, 125000000, 99999, 1149999500].map(brief), ['≈50 tBTC', '12.5 tBTC', '1.25 tBTC', '0.00099999 tBTC', '≈11.5 tBTC']);
|
|
});
|