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 } from '@noble/hashes/utils'; import { schnorr } from '@noble/curves/secp256k1'; import { bip85Entropy, childMnemonic, walletSecret, nostrKey, PATHS, newMnemonic } from '../src/adapters/lineage-keys.js'; import { createKeyring, addChild, graduate, walletOf, keysOf, nodeOf, mnemonicOf, pathOf, encodeKeyring, decodeKeyring } from '../src/adapters/keyring.js'; import { makeClaim, countersign, claimStatus, decodeClaim } from '../src/adapters/handoff.js'; import { TestWallet } from '../src/adapters/wallet.js'; import { LocalChain } from '../src/adapters/chain.js'; import { InscriptionAdapter } from '../src/adapters/inscription.js'; import { encodeLab, decodeLab } from '../src/storage.js'; const XPRV = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb'; const named = (ring, name) => ring.nodes.find(n => n.name === name && !n.retired); 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('the drawing as keys: deterministic, indices from the doc, seats as accounts, never reused', () => { const mnemonic = newMnemonic(), ring = createKeyring({ mnemonic }), again = createKeyring({ mnemonic }); const root = ring.nodes[0], wk = named(ring, 'well-known'), time = named(ring, 'time'), luna = named(ring, 'luna'), astra = named(ring, 'astra'); assert.deepEqual([wk.index, time.index, named(ring, 'docscope').index, named(ring, 'dollhouse').index, named(ring, 'lineage').index], [0, 1, 0, 1, 1]); assert.deepEqual([luna.account, astra.account], [1, 3]); assert.equal(mnemonicOf(ring, time), childMnemonic(mnemonic, 1)); assert.equal(walletOf(ring, time).address, walletOf(again, named(again, 'time')).address); assert.equal(keysOf(ring, luna).npub, nostrKey(mnemonicOf(ring, time), 1).npub); assert.notEqual(walletOf(ring, luna).address, walletOf(ring, time).address); assert.equal(new Set(ring.nodes.map(n => walletOf(ring, n).address)).size, ring.nodes.length); assert.match(pathOf(ring, named(ring, 'docscope')), /^lab root › 1' › 0' · m\/86'\/1'\/0'\/0\/0$/); assert.throws(() => addChild(ring, time.id, 'docscope'), /already exists/); assert.throws(() => addChild(ring, luna.id, 'x'), /Only orgs/); assert.throws(() => addChild(ring, root.id, 'Bad Name'), /lowercase/); const own = graduate(ring, named(ring, 'dollhouse').id); assert.equal(addChild(ring, time.id, 'phly').index, 2, 'the retired index 1 is not reused'); assert.equal(own.edge, 'adopted'); assert.equal(nodeOf(ring, own.from).retired, true); assert.match(pathOf(ring, own), /^own root \(dollhouse\) · /); assert.throws(() => graduate(ring, time.id), /leaves first/); assert.throws(() => graduate(ring, root.id), /derived edge/); assert.deepEqual(decodeKeyring(JSON.parse(JSON.stringify(encodeKeyring(ring)))), ring); assert.throws(() => decodeKeyring({ ...ring, root: 'not words' }), /mnemonic/); assert.throws(() => decodeKeyring({ ...ring, nodes: [...ring.nodes, { ...ring.nodes[1] }] }), /Duplicate/); const legacy = TestWallet.create(), withLegacy = createKeyring({ imported: legacy.secret, drawing: false }); assert.equal(walletOf(withLegacy, withLegacy.nodes[1]).address, legacy.address); }); test('graduation handoff: the venture claims, the parent countersigns, the inscription moves', () => { const ring = createKeyring(), chain = new LocalChain(); const time = walletOf(ring, named(ring, 'time')); chain.mine(time.script); const ins = new InscriptionAdapter(chain, time); const timeId = ins.inscribe('time', null, { lineage: 'root', name: 'time' }, { metaprotocol: 'lineage/v0' }); chain.mine(time.script); const docId = ins.inscribe('docscope', timeId, { lineage: 'birth', name: 'docscope' }, { metaprotocol: 'lineage/v0' }); chain.mine(time.script); const own = walletOf(ring, graduate(ring, named(ring, 'docscope').id)); assert.throws(() => makeClaim(chain, docId, own), /plain coin/); chain.mine(own.script); assert.throws(() => makeClaim(chain, docId, time), /already holds/); const claim = makeClaim(chain, docId, 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: time.pubkey }, time), /signature/i, 'a claim edited after signing fails'); assert.throws(() => countersign(chain, { ...claim, sig: '00'.repeat(64) }, time), /signature/i); const restored = decodeLab(encodeLab({ wallet: time, chain, keyring: ring, claims: [claim] })); assert.deepEqual(restored.claims, [claim]); assert.deepEqual(restored.keyring, ring); assert.equal(countersign(chain, claim, time), claim.txid); chain.mine(time.script); assert.ok(new InscriptionAdapter(chain, own).list().some(i => i.id === docId && i.parent === timeId)); assert.ok(!new InscriptionAdapter(chain, time).list().some(i => i.id === docId)); assert.ok(new InscriptionAdapter(chain, time).list().some(i => i.id === timeId), 'the parent keeps its own inscription'); assert.equal(claimStatus(chain, claim), 'stale'); assert.throws(() => countersign(chain, claim, time), /spent/); assert.deepEqual(decodeLab(encodeLab({ wallet: time, chain })).claims, []); });