import { CashuMint, CashuWallet, type MintQuoteResponse } from "@cashu/cashu-ts"; import type NDK from "@nostr-dev-kit/ndk"; import { type LnPaymentInfo, NDKEventId, type NDKPaymentConfirmationCashu, type NDKPaymentConfirmationLN, NDKPool, NDKPrivateKeySigner, NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, type NDKUser, } from "@nostr-dev-kit/ndk"; import createDebug from "debug"; import { EventEmitter } from "tseep"; import { mintProofs } from "../../utils/cashu.js"; import type { NutPayment } from "../cashu/pay/nut.js"; import { NDKWallet, type NDKWalletBalance, type NDKWalletEvents, NDKWalletStatus, type NDKWalletTypes, } from "../index.js"; import { MintInterface, getCashuWallet } from "../mint.js"; import { redeemNutzaps } from "./nutzap.js"; import { sendReq } from "./req.js"; import type { NDKNWCGetInfoResult, NDKNWCMakeInvoiceResult, NDKNWCRequestMap, NDKNWCResponseBase, NDKNWCResponseMap, } from "./types.js"; const d = createDebug("ndk-wallet:nwc"); export type NDKNWCWalletEvents = NDKWalletEvents & { connecting: () => void; error: () => void; timeout: (method: keyof NDKNWCRequestMap) => void; }; export class NDKNWCWallet extends NDKWallet { get type(): NDKWalletTypes { return "nwc"; } public status = NDKWalletStatus.INITIAL; public walletId = "nwc"; public pairingCode?: string; public walletService?: NDKUser; public relaySet?: NDKRelaySet; public signer?: NDKPrivateKeySigner; private _balance?: NDKWalletBalance; private cachedInfo?: NDKNWCGetInfoResult; public pool?: NDKPool; public timeout?: number; /** * * @param ndk * @param timeout A timeeout to use for all operations. */ constructor( ndk: NDK, { timeout, pairingCode, pubkey, relayUrls, secret, }: { timeout?: number; pairingCode?: string; pubkey?: string; relayUrls?: string[]; secret?: string; }, ) { super(ndk); if (pairingCode) { const u = new URL(pairingCode); pubkey = u.host ?? u.pathname; relayUrls = u.searchParams.getAll("relay"); secret = u.searchParams.get("secret") as string; this.pairingCode = pairingCode; } if (!pubkey || !relayUrls || !secret) throw new Error("Incomplete initialization parameters"); this.timeout = timeout; this.walletService = this.ndk.getUser({ pubkey }); this.pool = this.getPool(relayUrls); this.relaySet = NDKRelaySet.fromRelayUrls(relayUrls, this.ndk, true, this.pool); // Initialize signer this.signer = new NDKPrivateKeySigner(secret); this.pool.on("connect", () => { this.status = NDKWalletStatus.READY; this.emit("ready"); }); this.pool.on("relay:disconnect", () => (this.status = NDKWalletStatus.LOADING)); this.pool.connect(); if (this.pool.connectedRelays().length > 0) { this.status = NDKWalletStatus.READY; this.emit("ready"); } } private getPool(relayUrls: string[]) { for (const pool of this.ndk.pools) if (pool.name === "NWC") return pool; return new NDKPool(relayUrls, [], this.ndk, { name: "NWC" }); } async lnPay(payment: LnPaymentInfo): Promise { if (!this.signer) throw new Error("Wallet not initialized"); d("lnPay", payment.pr); // Create and sign NWC request event const res = await this.req("pay_invoice", { invoice: payment.pr }); d("lnPay res", res); if (res.result) { return { preimage: res.result.preimage, }; } this.updateBalance(); throw new Error(res.error?.message || "Payment failed"); } /** * Pay by minting tokens. * * This creates a quote on a mint, pays it using NWC and then mints the tokens. * * @param payment - The payment to pay * @param onLnPayment - A callback that is called when an LN payment will be processed * @returns The payment confirmation */ async cashuPay( payment: NutPayment, onLnInvoice?: (pr: string) => void, onLnPayment?: (mint: string, invoice: string) => void, ): Promise { if (!payment.mints) throw new Error("No mints provided"); for (const mint of payment.mints) { let amount = payment.amount; amount = amount / 1000; const wallet = new CashuWallet(new CashuMint(mint), { unit: "sat" }); let quote: MintQuoteResponse | undefined; try { quote = await wallet.createMintQuote(amount); d("cashuPay quote", quote); onLnInvoice?.(quote.request); } catch (e) { console.error("error creating mint quote", e); throw e; } if (!quote) throw new Error("Didnt receive a mint quote"); // todo check that the amount of the invoice matches the amount we want to pay try { const res = await this.req("pay_invoice", { invoice: quote.request }); if (res.result?.preimage) { onLnPayment?.(mint, res.result.preimage); } d("cashuPay res", res); } catch (e: any) { const message = e?.error?.message || e?.message || "unknown error"; console.error("error paying invoice", e, { message }); throw new Error(message); } this.updateBalance(); return mintProofs(wallet, quote, amount, mint, payment.p2pk); } } /** * Redeem a set of nutzaps into an NWC wallet. * * This function gets an invoice from the NWC wallet until the total amount of the nutzaps is enough to pay for the invoice * when accounting for fees. * * @param cashuWallet - The cashu wallet to redeem the nutzaps into * @param nutzaps - The nutzaps to redeem * @param proofs - The proofs to redeem * @param mint - The mint to redeem the nutzaps into * @param privkey - The private key needed to redeem p2pk proofs. */ public redeemNutzaps = redeemNutzaps.bind(this); /** * Fetch the balance of this wallet */ async updateBalance(): Promise { const res = await this.req("get_balance", {}); if (!res.result) throw new Error("Failed to get balance"); if (res.error) throw new Error(res.error.message); // update the cached balance property this._balance = { amount: res.result?.balance ?? 0, }; // balance is always in sats this._balance.amount /= 1000; this.emit("balance_updated"); } /** * Get the balance of this wallet */ get balance(): NDKWalletBalance | undefined { return this._balance; } req = sendReq.bind(this) as ( method: M, params: NDKNWCRequestMap[M], ) => Promise>; async getInfo(refetch = false) { if (refetch) { this.cachedInfo = undefined; } if (this.cachedInfo) return this.cachedInfo; const res = await this.req("get_info", {}); d("info", res); if (!res.result) throw new Error("Failed to get info"); if (res.error) throw new Error(res.error.message); this.cachedInfo = res.result; if (res.result.alias) this.walletId = res.result.alias; return res.result; } async listTransactions() { const res = await this.req("list_transactions", {}); if (!res.result) throw new Error("Failed to list transactions"); return res.result; } async makeInvoice(amount: number, description: string): Promise { const res = await this.req("make_invoice", { amount, description }); if (!res.result) throw new Error("Failed to make invoice"); return res.result; } }