import { CashuMint, CashuWallet, MintQuoteState, type Proof, } from "@cashu/cashu-ts"; import { privateKeyFromSeedWords as nostrPrivateKeyFromSeedWords } from "nostr-tools/nip06"; import { finalizeEvent, nip98 } from "nostr-tools"; import type { BodyInit } from "bun"; interface NpubCashQuote { createdAt: number; paidAt?: number; expiresAt: number; mintUrl: string; quoteId: string; request: string; amount: number; state: "PAID" | "ISSUED" | "INFLIGHT"; locked: boolean; } interface NpubCashPaginationMetadata { total: number; limit: number; } export default class NpubCash { constructor(private mnemonic: string) {} private async fetchWithNip98( { url, method, body }: { url: string; method: string; body?: BodyInit | null; }, ): Promise { const nostrPrivateKey = nostrPrivateKeyFromSeedWords(this.mnemonic); const urlWithoutQueryParams = url.split("?")[0] ?? ""; const npubCashNip98 = await nip98.getToken( urlWithoutQueryParams, method, (event) => finalizeEvent(event, nostrPrivateKey), ); return fetch(url, { method, headers: { "Authorization": `Nostr ${npubCashNip98}`, }, body, }).then((data) => data.json() as Promise); } private async getQuotes(since: number, limit: number, offset: number) { return this.fetchWithNip98< { error: false; data: { quotes: NpubCashQuote[] }; metadata: NpubCashPaginationMetadata; } | { error: true; message: string; } >({ url: `https://npubx.cash/api/v2/wallet/quotes?since=${since}&limit=${limit}&offset=${offset}`, method: "GET", }); } private async getAllPaidQuotes(since: number) { const quotes: NpubCashQuote[] = []; let offset = 0; while (true) { const currentQuotes = await this.getQuotes(since, 50, offset); if (currentQuotes.error === false) { quotes.push(...currentQuotes.data.quotes); if (quotes.length >= currentQuotes.metadata.total) { break; } } else { throw new Error(currentQuotes.message ?? "Unknown error"); } offset += 50; } return quotes.sort((a, b) => a.createdAt - b.createdAt).filter( (quote) => quote.state === "PAID", ); } async *tryRedeemUnredeemedCashuQuotes( latestRedeemedCashuQuoteTimestamp: number, ) { const proofs: Proof[] = []; const quotes: NpubCashQuote[] = await this.getAllPaidQuotes( latestRedeemedCashuQuoteTimestamp, ); for (const quote of quotes) { const mint = new CashuMint(quote.mintUrl); const wallet = new CashuWallet(mint); const req = await mint.checkMintQuote(quote.quoteId); if ( req.state === MintQuoteState.PAID && typeof quote.paidAt === "number" ) { const newProofs = await wallet.mintProofs( quote.amount, quote.quoteId, ); proofs.push(...newProofs); const amountReceived = newProofs.reduce( (sum, p) => sum + p.amount, 0, ); yield { quoteId: quote.quoteId, amountSat: amountReceived, timestamp: quote.paidAt, }; } } return proofs; } }