import {afterEach, expect} from 'vitest' import {createMockMint} from 'lnurlcash-conformance/mock-mint' import {bytesToHex, hexToBytes} from '@noble/hashes/utils.js' import {sha256} from '@noble/hashes/sha2.js' import {buildNoteUrl, fetchNoteInfo} from 'lnurlcash-kit' import type {Bearer} from './types' import {requiredValue} from './test-utils' export type Mint = Awaited> const mints: Mint[] = [] export const mint = async (options: Parameters[0] = {}): Promise => { const instance = await createMockMint(options) mints.push(instance) return instance } afterEach(async () => { await Promise.all(mints.splice(0).map((instance) => instance.close())) }) export const secret = (seed: string): string => bytesToHex(sha256(hexToBytes('00'.repeat(31) + seed))) export const persistOutput = async (): Promise => undefined // deterministic stand-in for the wallet's BIP-32 allocation path: answers // every reservation with fresh unique secrets, never reusing one let allocatedSecretCounter = 0 export const allocateOutputSecrets = ( _server: string, count: number, ): Promise => { const secrets = Array.from({length: count}, () => { allocatedSecretCounter += 1 return secret(`allocated-${allocatedSecretCounter.toString(16)}`) }) return Promise.resolve(secrets) } // the receive-rotation staging checkpoint - engine tests keep no wallet, // so staging is a recorded no-op for them export const stageRotation = async (): Promise => undefined // the trusted signing keys a production caller would serve from the // trusted-mint registry: the mock mint's current key plus any it retired export const signatureKeysFor = (instance: Mint) => (): readonly string[] => [instance.state.pubkey, ...instance.state.previousPubkeys] export const noteUrl = (instance: Mint, k1: string, amountMsat?: number): string => buildNoteUrl(`${instance.url}/w`, k1, amountMsat) let fixtureCounter = 0 export const makeBearer = async ( instance: Mint, k1: string, amountMsat: number, ): Promise => { instance.state.creditNote(k1, amountMsat) const url = noteUrl(instance, k1, amountMsat) const info = await fetchNoteInfo(url) fixtureCounter += 1 return { id: `fixture-${fixtureCounter}`, url, callback: info.callback, amount: info.maxWithdrawable, verified: true, mintPubkey: instance.state.pubkey, createdAt: Date.now(), updatedAt: Date.now(), } } export const settleLastInvoice = async (instance: Mint): Promise => { const paymentHash = requiredValue([...instance.state.invoices.keys()].at(-1)) const response = await fetch(`${instance.url}/_test/settle?payment_hash=${paymentHash}`) if (!response.ok) throw new Error(`settle hook failed: ${response.status}`) return requiredValue(instance.state.invoices.get(paymentHash)).preimage } export const settleWhenRequested = async (instance: Mint): Promise => { for (let attempt = 0; attempt < 200 && instance.state.invoices.size === 0; attempt++) { await new Promise((resolve) => setTimeout(resolve, 10)) } return settleLastInvoice(instance) } export const expectBurned = async (instance: Mint, k1: string): Promise => { for (let attempt = 0; attempt < 200 && instance.state.noteState(k1) !== 'burned'; attempt++) { await new Promise((resolve) => setTimeout(resolve, 10)) } expect(instance.state.noteState(k1)).toBe('burned') } // a fetch wrapper that answers every /w/cb request with the response to // its SECOND, byte-identical send - the HTTP-stack GET retry a real client // is subject to: the mint executes the first attempt and (with the mock's // default retriedMutation: 'refuse', which is also lnurl-mint's behavior) // refuses the repeat as an already-spent input, which is all the caller // ever sees export const retryingCbFetch = (): typeof fetch => { const impl: typeof fetch = async (input, init) => { const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url if (url.includes('/w/cb')) { const first = await fetch(input, init) await first.arrayBuffer() return fetch(input, init) } return fetch(input, init) } return impl }