Mining is a lab setting. Split (the default) pays every active holding a share of each beat, pro rata by a hashrate dial on each holding (0 to 9, 1 unless set), one coinbase output per holding. Last act gives the whole beat to the holding that acts. Every act is paid by the acting node's own keys: fees and postage come out of its gas tank (the plain coins at its Taproot key), which tops up from its own money 0.0001 test BTC at a time. Each act is checked before anything is signed, so an unfunded node can't act. A graduating org pays for its own claim, and the lab wallet pays only for foundings and for holdings that predate lab keys. Sends cross holdings, the ledger shows gas top-ups, and chain keys are derived once per page. The engine's second local change: a coinbase may split its reward across up to 64 outputs (mineTo), in src/adapters/chain.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
236 lines
17 KiB
JavaScript
236 lines
17 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { HDKey } from '@scure/bip32';
|
||
import { entropyToMnemonic } from '@scure/bip39';
|
||
import { wordlist } from '@scure/bip39/wordlists/english';
|
||
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';
|
||
import { sha256 } from '@noble/hashes/sha2';
|
||
import { schnorr } from '@noble/curves/secp256k1';
|
||
import { base64 } from '@scure/base';
|
||
import { Address, Tap } from '@cmdcode/tapscript';
|
||
import { PATHS, bip85Entropy, childMnemonic, nostrKey, nostrSecret, seedBytes, segwitPubkey, segwitSecret, walletSecret } from '../src/adapters/lineage-keys.js';
|
||
import { claimStatus, countersign, decodeClaim, holderCoin, makeClaim } from '../src/adapters/handoff.js';
|
||
import { LocalChain } from '../src/adapters/chain.js';
|
||
import { InscriptionAdapter } from '../src/adapters/inscription.js';
|
||
import { TestWallet } from '../src/adapters/wallet.js';
|
||
import { beat, decodeLab, emptyLab, encodeLab, keysPreference } from '../src/orgchain/store.js';
|
||
import { act, fund, lineageState, send } from '../src/orgchain/actions.js';
|
||
import { SAT } from '../src/orgchain/money.js';
|
||
import { addressesOf, descsum, keyOf, leavesOf, nsecOf, rootWallet, scriptHex, secretsOf, tapkeyOf, wif } from '../src/orgchain/keys.js';
|
||
import { agentLeaves, agentRecords, agentSecrets, nodeLeaves, nodeRecords, nodeSecrets, sshFingerprint } from '../src/orgchain/leaves.js';
|
||
|
||
// Spec vectors and the handoff test come from the tree branch (2ea7884), which wrote lineage-keys.js and handoff.js.
|
||
const XPRV = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb';
|
||
test('BIP-85 spec vectors: core entropy and the 24-word BIP-39 application', () => {
|
||
const root = HDKey.fromExtendedKey(XPRV);
|
||
assert.equal(bytesToHex(root.derive("m/83696968'/0'/0'").privateKey), 'cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0');
|
||
assert.equal(bytesToHex(bip85Entropy(root, "m/83696968'/0'/0'")), 'efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7');
|
||
const entropy = bip85Entropy(root, PATHS.orgSeed(0)).slice(0, 32);
|
||
assert.equal(bytesToHex(entropy), 'ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f');
|
||
assert.equal(entropyToMnemonic(entropy, wordlist), 'puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano');
|
||
});
|
||
test('BIP-86 and NIP-06 spec vectors', () => {
|
||
const abandon = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||
assert.equal(bytesToHex(schnorr.getPublicKey(walletSecret(abandon, 0, 0))), 'cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115');
|
||
assert.deepEqual(nostrKey('leader monkey parrot ring guide accident before fence cannon height naive bean', 0), {
|
||
pubkey: '17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917', npub: 'npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu' });
|
||
});
|
||
test('handoff: the recipient claims, the holder countersigns, the inscription moves', () => {
|
||
const chain = new LocalChain(), holder = TestWallet.create(), own = TestWallet.create();
|
||
chain.mine(holder.script);
|
||
const ins = new InscriptionAdapter(chain, holder);
|
||
const top = ins.inscribe('time', null, { op: 'found', name: 'time' }, { metaprotocol: 'lineage/v0' });
|
||
const doc = ins.inscribe('docscope', top, { op: 'birth', name: 'docscope' }, { metaprotocol: 'lineage/v0' });
|
||
chain.mine(holder.script);
|
||
assert.throws(() => makeClaim(chain, doc, own), /plain coin/);
|
||
chain.mine(own.script);
|
||
assert.throws(() => makeClaim(chain, doc, holder), /already holds/);
|
||
const claim = makeClaim(chain, doc, own);
|
||
assert.equal(claimStatus(chain, claim), 'open');
|
||
assert.deepEqual(decodeClaim(JSON.parse(JSON.stringify(claim))), claim);
|
||
assert.throws(() => countersign(chain, claim, own), /current holder/);
|
||
assert.throws(() => countersign(chain, { ...claim, to: holder.pubkey }, holder), /signature/i, 'a claim edited after signing fails');
|
||
assert.equal(countersign(chain, claim, holder), claim.txid);
|
||
chain.mine(holder.script);
|
||
assert.ok(new InscriptionAdapter(chain, own).list().some(i => i.id === doc && i.parent === top));
|
||
assert.ok(new InscriptionAdapter(chain, holder).list().some(i => i.id === top), 'the parent keeps its own inscription');
|
||
assert.equal(claimStatus(chain, claim), 'stale');
|
||
assert.throws(() => countersign(chain, claim, holder), /spent/);
|
||
});
|
||
|
||
|
||
// Values read off the lineage key tree page (lineage.yolopreneur.lol) for this mnemonic: agent account 1, lineage.llc.
|
||
const LEADER = 'leader monkey parrot ring guide accident before fence cannon height naive bean';
|
||
const ABANDON = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||
test('BIP-86 and BIP-84: the spec mnemonic gives the spec addresses', () => {
|
||
const tapkey = tapkeyOf('cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115');
|
||
assert.equal(tapkey, 'a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c');
|
||
const wallet = new TestWallet(Tap.getSecKey(walletSecret(ABANDON, 0, 0))[0]);
|
||
assert.equal(wallet.pubkey, tapkey);
|
||
assert.equal(Address.p2tr.fromPubKey(wallet.pubkey, 'main'), 'bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr');
|
||
assert.equal(Address.p2wpkh.fromPubKey(segwitPubkey(ABANDON, 0, 0), 'main'), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
|
||
});
|
||
test('leaves match the lineage key tree page byte for byte', () => {
|
||
const agent = agentLeaves(nostrSecret(LEADER, 1));
|
||
assert.equal(nostrKey(LEADER, 1).npub, 'npub1m9m6dnc0svwugus8sz6l29rqatmdegywxtgld6ymvq6y6ca0fczq88tsjr');
|
||
assert.deepEqual([sshFingerprint(agent.ssh), sshFingerprint(agent.git), base64.encode(agent.dkim), base64.encode(agent.wg)], [
|
||
'SHA256:Svt6sRSfHEYSQf9+QX+XDPg9eXlEsgcy+OAcJIzeyQU', 'SHA256:fBH0g7huqXVWQHa6GiDsMMzg2PTnWwyTgJdrH3L5bo4',
|
||
'A7j292wHd4jUXhs4dMJcKh0nDG1/cwoo4aG1NUn/x2g=', '0HVH6534UBMEVWDz8BA+NkI73Z28bHBBQNFEONCkmWI=']);
|
||
assert.deepEqual(nodeRecords(nodeLeaves(seedBytes(LEADER)), 'lineage.llc').map(([, value]) => value), [
|
||
'SHA256:XOvpZ24mpcsFhi0mDxyZDqSEpkVKsQkhnLCOR5mlxY4', 'ObnNHsWk0TI5evUH+wbuMXtyPtEXInA6Y0KJbFu8S7A=',
|
||
'mkey:e35077cb9d3e78ad67898407caa1a059453fcec698f00f3585c6d87e7950e676']);
|
||
const lines = Object.fromEntries(agentRecords(agent, 'sol@lineage.llc', 'sol', 'lineage.llc').map(([label, , line]) => [label, line]));
|
||
assert.match(lines.ssh, /^ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI[A-Za-z0-9+/]{43} sol@lineage\.llc$/);
|
||
assert.match(lines.git, /^sol@lineage\.llc namespaces="git" ssh-ed25519 \S+ sol@lineage\.llc$/);
|
||
assert.equal(lines.dkim, 'sol._domainkey.lineage.llc. IN TXT "v=DKIM1; k=ed25519; p=A7j292wHd4jUXhs4dMJcKh0nDG1/cwoo4aG1NUn/x2g="');
|
||
assert.equal(lines.wg, '[Peer]\n# sol@lineage.llc\nPublicKey = 0HVH6534UBMEVWDz8BA+NkI73Z28bHBBQNFEONCkmWI=');
|
||
});
|
||
|
||
const sha16 = text => bytesToHex(sha256(utf8ToBytes(text))).slice(0, 16);
|
||
test('secrets match the lineage key tree page byte for byte', () => {
|
||
const page = { ssh: '39b2f5573e3bce0b', git: 'dc59220f725526ac', dkim: 'd3bd4ee4fab19a5e', wg: 'def16367a9dcc19a', 'ssh-ca': 'ac0e7b5448ba3818', mtls: '9ef175861cefe59d', noise: 'bea4ba0c87cc7707' };
|
||
const ours = [...agentSecrets(nostrSecret(LEADER, 1), 'agent-a@lineage.llc'), ...nodeSecrets(seedBytes(LEADER), 'lineage.llc')];
|
||
assert.deepEqual(Object.fromEntries(ours.map(([label, , text]) => [label, sha16(text)])), page);
|
||
// The page shows mainnet forms (WIF version 0x80); the lab's own descriptors use test networks.
|
||
assert.deepEqual([nsecOf(nostrSecret(LEADER, 1)), descsum(`tr(${wif(nostrSecret(LEADER, 1), 0x80)})`), descsum(`tr(${wif(walletSecret(LEADER, 1, 0), 0x80)})`),
|
||
descsum(`wpkh(${wif(segwitSecret(LEADER, 1, 0), 0x80)})`)].map(sha16), ['8d07c2e20021a973', '4b4d32db9ac69c8d', '1908b363e70a2120', '5caff55640aca16d']);
|
||
assert.equal(wif(segwitSecret(ABANDON, 0, 0), 0x80), 'KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d', 'BIP-84 vector');
|
||
assert.equal(descsum('raw(deadbeef)'), 'raw(deadbeef)#89f8spxm', 'BIP-380 vector');
|
||
assert.match(ours[0][2], /^-----BEGIN OPENSSH PRIVATE KEY-----\n[A-Za-z0-9+/=\n]+\n-----END OPENSSH PRIVATE KEY-----\n$/);
|
||
});
|
||
|
||
const at = (lab, id) => scriptHex(holderCoin(lab.chain, id).scriptPubKey);
|
||
const key = (mnemonic, account) => `5120${new TestWallet(Tap.getSecKey(walletSecret(mnemonic, account))[0]).pubkey}`;
|
||
|
||
test('each holding has its own root, and every node holds its inscription at its Taproot address', () => {
|
||
const lab = emptyLab();
|
||
const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' });
|
||
send(lab, root, ops, SAT); // every act pays its own gas
|
||
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }), lead = act(lab, 'birth', root, { name: 'lead', kind: 'agent' });
|
||
const m = lab.keys.roots[root], opsSeed = childMnemonic(m, 0);
|
||
assert.equal(m.split(' ').length, 24);
|
||
assert.deepEqual([root, ops, bot, lead].map(id => at(lab, id)), [key(m, 0), key(opsSeed, 0), key(opsSeed, 1), key(m, 1)]);
|
||
const s = lineageState(lab), k = keyOf(lab.keys, s, s.nodes.get(bot)), a = addressesOf(lab.keys, s, s.nodes.get(bot));
|
||
assert.equal(k.path, "acme root › 0' · m/86'/1'/1'/0/0");
|
||
assert.deepEqual([k.from, k.account, k.scheme], ["acme root › 0'", 1, 'bip86']);
|
||
assert.deepEqual(a, {
|
||
taproot: { address: Address.p2tr.fromPubKey(key(opsSeed, 1).slice(4), 'regtest'), path: "m/86'/1'/1'/0/0" },
|
||
segwit: { address: Address.p2wpkh.fromPubKey(segwitPubkey(opsSeed, 1), 'regtest'), path: "m/84'/1'/1'/0/0" },
|
||
tapkey: { address: Address.p2tr.fromPubKey(tapkeyOf(nostrKey(opsSeed, 1).pubkey), 'regtest'), path: "m/44'/1237'/1'/0/0" },
|
||
npub: { address: nostrKey(opsSeed, 1).npub, path: "m/44'/1237'/1'/0/0" },
|
||
});
|
||
assert.match(a.taproot.address, /^bcrt1p/);
|
||
assert.match(a.segwit.address, /^bcrt1q/);
|
||
const award = act(lab, 'award', ops, { to: bot, skill: 'launch', belt: 'green' });
|
||
assert.equal(at(lab, award), key(opsSeed, 0), 'an act is signed by its authorizer, and its record stays with that key');
|
||
const other = act(lab, 'found', null, { name: 'other' });
|
||
assert.notEqual(lab.keys.roots[other], m);
|
||
});
|
||
|
||
test('leaves hang off the same tree: agents from their nostr secret, orgs from their seed', () => {
|
||
const lab = emptyLab();
|
||
const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' });
|
||
send(lab, root, ops, SAT);
|
||
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' });
|
||
const opsSeed = childMnemonic(lab.keys.roots[root], 0), s = lineageState(lab), node = id => s.nodes.get(id);
|
||
assert.deepEqual(leavesOf(lab.keys, s, node(bot)), agentRecords(agentLeaves(nostrSecret(opsSeed, 1)), 'bot@ops.acme', 'bot', 'ops.acme'));
|
||
assert.deepEqual(leavesOf(lab.keys, s, node(ops)), nodeRecords(nodeLeaves(seedBytes(opsSeed)), 'ops.acme'));
|
||
assert.deepEqual(leavesOf(lab.keys, s, node(root)).map(([label]) => label), ['ssh-ca', 'mtls', 'noise']);
|
||
assert.match(leavesOf(lab.keys, s, node(root))[0][2], / lineage-ca@acme$/);
|
||
});
|
||
|
||
test('secrets: an org shows its seed, a seat only its account, and every descriptor imports as it is', () => {
|
||
const lab = emptyLab();
|
||
const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' });
|
||
send(lab, root, ops, SAT);
|
||
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' });
|
||
const m = lab.keys.roots[root], opsSeed = childMnemonic(m, 0), s = lineageState(lab), node = id => s.nodes.get(id);
|
||
assert.deepEqual(secretsOf(lab.keys, s, node(root)).seed, { from: 'acme root', mnemonic: m });
|
||
assert.deepEqual(secretsOf(lab.keys, s, node(ops)).seed, { from: "acme root › 0'", mnemonic: opsSeed });
|
||
const seat = secretsOf(lab.keys, s, node(bot));
|
||
assert.equal(seat.seed, null);
|
||
assert.deepEqual(seat.account.map(([label]) => label), ['nsec', 'taproot', 'segwit', 'tapkey']);
|
||
assert.deepEqual(seat.account.slice(1).map(([, , d]) => d), [descsum(`tr(${wif(walletSecret(opsSeed, 1))})`),
|
||
descsum(`wpkh(${wif(segwitSecret(opsSeed, 1))})`), descsum(`tr(${wif(nostrSecret(opsSeed, 1))})`)]);
|
||
assert.match(seat.account[1][2], /^tr\(c[1-9A-HJ-NP-Za-km-z]{51}\)#[a-z0-9]{8}$/, 'test-network WIF');
|
||
assert.deepEqual(seat.leaves, agentSecrets(nostrSecret(opsSeed, 1), 'bot@ops.acme'));
|
||
assert.deepEqual(secretsOf(lab.keys, s, node(ops)).leaves, nodeSecrets(seedBytes(opsSeed), 'ops.acme'));
|
||
assert.equal(addressesOf(lab.keys, s, node(bot)).tapkey.address, Address.p2tr.fromPubKey(tapkeyOf(nostrKey(opsSeed, 1).pubkey), 'regtest'), 'the npub alone finds the tapkey');
|
||
});
|
||
|
||
test('graduation: a fresh root claims the org, the derived key countersigns, and the subtree re-keys', () => {
|
||
const lab = emptyLab();
|
||
const root = act(lab, 'found', null, { name: 'acme' }), studio = act(lab, 'birth', root, { name: 'studio' });
|
||
send(lab, root, studio, 2 * SAT);
|
||
const kid = act(lab, 'birth', studio, { name: 'kid' });
|
||
send(lab, studio, kid, SAT);
|
||
const deep = act(lab, 'birth', kid, { name: 'deep' });
|
||
const seat = act(lab, 'birth', studio, { name: 'seat', kind: 'agent' }), solo = act(lab, 'birth', studio, { name: 'solo' });
|
||
send(lab, studio, solo, 0.1 * SAT); // a graduate pays for the claim on its own root
|
||
const old = childMnemonic(lab.keys.roots[root], 0);
|
||
act(lab, 'graduate', studio, { of: solo }); // solo graduates first and keeps its own root through what follows
|
||
const soloRoot = lab.keys.adopted[solo];
|
||
act(lab, 'graduate', root, { of: studio });
|
||
const fresh = lab.keys.adopted[studio];
|
||
assert.ok(fresh && fresh !== old);
|
||
assert.deepEqual([studio, kid, deep, seat, solo].map(id => at(lab, id)),
|
||
[key(fresh, 0), key(childMnemonic(fresh, 0), 0), key(childMnemonic(childMnemonic(fresh, 0), 0), 0), key(fresh, 1), key(soloRoot, 0)]);
|
||
const leftovers = lab.chain.coins(rootWallet(lab.keys, old).script).flatMap(c => (c.inscriptions ?? []).map(i => i.id));
|
||
assert.deepEqual(leftovers.filter(id => [studio, kid, deep, seat].includes(id)), []);
|
||
const decision = act(lab, 'decide', studio, { title: 'Our own root now' });
|
||
assert.equal(at(lab, decision), key(fresh, 0), 'the org now signs with its own root');
|
||
const s = lineageState(lab);
|
||
assert.equal(keyOf(lab.keys, s, s.nodes.get(seat)).path, "studio root · m/86'/1'/1'/0/0");
|
||
assert.match(leavesOf(lab.keys, s, s.nodes.get(seat))[0][2], / seat@studio\.acme$/, 'names still nest after graduation');
|
||
assert.deepEqual(decodeLab(encodeLab(lab)).keys, lab.keys);
|
||
});
|
||
|
||
test('a lab keyed by v1.2 keeps its npub-tweaked chain keys', () => {
|
||
const lab = emptyLab();
|
||
lab.keys.scheme = 'tapkey';
|
||
const root = act(lab, 'found', null, { name: 'mid' }), seat = act(lab, 'birth', root, { name: 'seat', kind: 'agent' });
|
||
const m = lab.keys.roots[root];
|
||
assert.deepEqual([root, seat].map(id => at(lab, id)), [0, 1].map(a => `5120${tapkeyOf(nostrKey(m, a).pubkey)}`));
|
||
const s = lineageState(lab), k = keyOf(lab.keys, s, s.nodes.get(seat));
|
||
assert.equal(k.path, "mid root · m/44'/1237'/1'/0/0");
|
||
assert.equal(addressesOf(lab.keys, s, s.nodes.get(seat)).segwit.path, "m/84'/1'/1'/0/0", 'every lab lists SegWit');
|
||
const v12 = secretsOf(lab.keys, s, s.nodes.get(seat)).account;
|
||
assert.equal(v12[1][2], v12[3][2], 'in a v1.2 lab the Taproot key is the tapkey');
|
||
assert.equal(decodeLab(encodeLab(lab)).keys.scheme, 'tapkey');
|
||
});
|
||
|
||
test('a lab keyed by v1.1 keeps its separate, untweaked chain keys', () => {
|
||
const lab = emptyLab();
|
||
lab.keys.scheme = 'wallet';
|
||
const root = act(lab, 'found', null, { name: 'old' }), seat = act(lab, 'birth', root, { name: 'seat', kind: 'agent' });
|
||
const m = lab.keys.roots[root];
|
||
assert.deepEqual([root, seat].map(id => at(lab, id)), [0, 1].map(a => `5120${new TestWallet(walletSecret(m, a)).pubkey}`));
|
||
const s = lineageState(lab), k = keyOf(lab.keys, s, s.nodes.get(seat));
|
||
assert.equal(k.path, "old root · m/86'/1'/1'/0/0");
|
||
assert.equal(k.scheme, 'wallet');
|
||
assert.match(secretsOf(lab.keys, s, s.nodes.get(seat)).account[1][2], /^rawtr\(c/, 'an untweaked key imports as rawtr()');
|
||
const saved = JSON.parse(encodeLab(lab));
|
||
delete saved.keys.scheme; // v1.1 saved roots and no scheme
|
||
assert.equal(decodeLab(JSON.stringify(saved)).keys.scheme, 'wallet');
|
||
assert.throws(() => keysPreference({ scheme: 'nope' }), /Invalid lab keys/);
|
||
});
|
||
|
||
test('holdings from before lab keys still act, with the one lab wallet', () => {
|
||
const lab = emptyLab();
|
||
fund(lab);
|
||
const legacy = new InscriptionAdapter(lab.chain, lab.wallet).inscribe('found old', null, { op: 'found', name: 'old' }, { metaprotocol: 'lineage/v0' });
|
||
beat(lab);
|
||
const kid = act(lab, 'birth', legacy, { name: 'kid' });
|
||
assert.equal(at(lab, kid), `5120${lab.wallet.pubkey}`);
|
||
act(lab, 'graduate', legacy, { of: kid });
|
||
assert.equal(at(lab, kid), `5120${lab.wallet.pubkey}`, 'without a root there is nothing to re-key; the event is still recorded');
|
||
const s = lineageState(lab);
|
||
assert.equal(keyOf(lab.keys, s, s.nodes.get(kid)), null);
|
||
assert.equal(leavesOf(lab.keys, s, s.nodes.get(kid)), null);
|
||
assert.equal(s.nodes.get(kid).edge, 'adopted');
|
||
const saved = JSON.parse(encodeLab(lab));
|
||
delete saved.keys;
|
||
assert.deepEqual(decodeLab(JSON.stringify(saved)).keys, { scheme: 'bip86', roots: {}, adopted: {} });
|
||
assert.throws(() => keysPreference({ roots: { [`${'a'.repeat(64)}i0`]: 'not a mnemonic' } }), /Invalid lab mnemonic/);
|
||
});
|