diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/dnssec-test.js | 39 | ||||
| -rw-r--r-- | tools/noffer-test.js | 99 |
2 files changed, 138 insertions, 0 deletions
diff --git a/tools/dnssec-test.js b/tools/dnssec-test.js new file mode 100644 index 0000000..71ebcee --- /dev/null +++ b/tools/dnssec-test.js @@ -0,0 +1,39 @@ +// DNSSEC chain validation against LIVE DNS. Needs network. +// Run: bun tools/dnssec-test.js +import { resolveTxtValidated } from '../src/dnssec.js'; + +let fails = 0; +const check = (name, ok, detail = '') => { + console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 140) : ''}`); + if (!ok) fails++; +}; + +// The canonical BIP-353 record: ₿[email protected] (alg 8 chain). +try { + const { txts } = await resolveTxtValidated('matt.user._bitcoin-payment.mattcorallo.com'); + const uri = txts.find((t) => t.toLowerCase().startsWith('bitcoin:')); + check('validates [email protected] (RSA chain)', !!uri, (uri || '').slice(0, 60) + '…'); +} catch (e) { check('validates [email protected] (RSA chain)', false, e.message); } + +// A Cloudflare-signed TXT (alg 13 chain): cloudflare.com SPF record. +try { + const { txts } = await resolveTxtValidated('cloudflare.com'); + check('validates cloudflare.com TXT (ECDSA chain)', txts.length > 0, txts[0]); +} catch (e) { check('validates cloudflare.com TXT (ECDSA chain)', false, e.message); } + +// An unsigned zone must FAIL closed, not pass quietly. +try { + await resolveTxtValidated('google.com'); // google.com is famously unsigned + check('unsigned zone fails closed', false, 'validated?!'); +} catch (e) { + check('unsigned zone fails closed', /not in a signed zone|no DNSKEY|fails/.test(e.message), e.message); +} + +// A missing record must throw, not return empty-and-valid. +try { + await resolveTxtValidated('definitely-not-real.user._bitcoin-payment.mattcorallo.com'); + check('missing record throws', false); +} catch (e) { check('missing record throws', true, e.message); } + +console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ DNSSEC validation behaves'); +process.exit(fails ? 1 : 0); diff --git a/tools/noffer-test.js b/tools/noffer-test.js new file mode 100644 index 0000000..152ef0b --- /dev/null +++ b/tools/noffer-test.js @@ -0,0 +1,99 @@ +// CLINK offer codes + the offer service: TLV roundtrip, and a full +// kind-21001 request/response through the nwc feature with a fake transport. +// Run: bun tools/noffer-test.js +import { hex } from '@scure/base'; +import * as nip44 from 'nostr-tools/nip44'; +import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; +import { encodeNoffer, decodeNoffer } from '../src/noffer.js'; +import { nwcFeature } from '../src/features/nwc.js'; + +let fails = 0; +const check = (name, ok, detail = '') => { + console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 120) : ''}`); + if (!ok) fails++; +}; + +console.log('[offer code TLV]'); +const pk = getPublicKey(generateSecretKey()); +const code = encodeNoffer({ pubkey: pk, relay: 'wss://relay.coinos.io', offerId: 'zap_default', priceType: 2 }); +check('encodes to noffer1…', code.startsWith('noffer1'), code.slice(0, 24)); +const dec = decodeNoffer(code); +check('pubkey roundtrips', dec.pubkey === pk); +check('relay roundtrips', dec.relay === 'wss://relay.coinos.io'); +check('offer id roundtrips', dec.offerId === 'zap_default'); +check('price type roundtrips', dec.priceType === 2); + +console.log('\n[offer service]'); +// harness mirroring tools/nwc-test.js +const store = {}; +let subHandlers = []; // every listen() subscription callback +const published = []; +const nwcTransport = { + subscribe: (relays, filter, on) => { subHandlers.push({ filter, on }); return () => {}; }, + publish: async (relays, evt) => { published.push(evt); return true; }, + query: async () => [], +}; +const wallet = { + watchOnly: false, + loadFeatureState: (n, fb) => JSON.parse(store['fs:' + n] || 'null') || fb, + saveFeatureState: (n, st) => { store['fs:' + n] = JSON.stringify(st); }, + _cacheKey: () => 'w1', + account: () => null, + saveCache: () => {}, + registerCacheExtension: () => {}, +}; +const hooks = { + arkReady: () => true, + arkMakeInvoice: async (sat, desc) => ({ invoice: 'lnbc_mock_' + sat, paymentHash: 'cd'.repeat(32), amountSat: sat }), + arkMovements: () => [], + arkBgWrite: async () => {}, + arkBgSpendableSat: async () => 0, +}; +const ctx = { + h: () => null, ui: {}, render: () => {}, wallet, + hook: (n, ...a) => (hooks[n] ? hooks[n](...a) : null), + fmtAmount: String, unitLabel: () => 'sats', copyBtn: () => null, toast: () => {}, nwcTransport, +}; +const f = nwcFeature(ctx); +f.init(); +await new Promise((r) => setTimeout(r, 50)); + +const offerCode = f.nwcOfferString(); +check('feature mints an offer code', !!offerCode && offerCode.startsWith('noffer1'), (offerCode || '').slice(0, 24)); +const offer = decodeNoffer(offerCode); +const offerSub = subHandlers.find((s2) => (s2.filter.kinds || []).includes(21001)); +check('subscribed for offer requests', !!offerSub && offerSub.filter['#p'][0] === offer.pubkey); + +// a payer requests 21 sats with a zap attached +const payerSk = generateSecretKey(); +const zapReq = finalizeEvent({ kind: 9734, created_at: Math.floor(Date.now() / 1000), + tags: [['p', 'ab'.repeat(32)], ['relays', 'wss://relay.damus.io']], content: '' }, payerSk); +const reqPayload = { offer: 'zap_default', amount_sats: 21, zap: JSON.stringify(zapReq) }; +const convKey = nip44.getConversationKey(payerSk, offer.pubkey); +const reqEv = finalizeEvent({ + kind: 21001, created_at: Math.floor(Date.now() / 1000), + tags: [['p', offer.pubkey], ['clink_version', '1']], + content: nip44.encrypt(JSON.stringify(reqPayload), convKey), +}, payerSk); +await offerSub.on(reqEv); +await new Promise((r) => setTimeout(r, 100)); + +const resp = published.find((e) => e.kind === 21001); +check('offer request answered', !!resp); +if (resp) { + const body = JSON.parse(nip44.decrypt(resp.content, convKey)); + check('response carries the invoice', body.bolt11 === 'lnbc_mock_21', JSON.stringify(body)); + check('response e-tags the request', resp.tags.some((t) => t[0] === 'e' && t[1] === reqEv.id)); + check('response signed by the offer key', resp.pubkey === offer.pubkey); +} +const st = JSON.parse(store['fs:nwc']); +check('zap held for its receipt', (st.zapPending || []).length === 1 && st.zapPending[0].invoice === 'lnbc_mock_21'); + +// replay: same event must not be answered twice +const before = published.filter((e) => e.kind === 21001).length; +await offerSub.on(reqEv); +await new Promise((r) => setTimeout(r, 60)); +check('replayed request ignored', published.filter((e) => e.kind === 21001).length === before); + +console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ offer service behaves'); +process.exit(fails ? 1 : 0); |
