UNPKG

askexperts

Version:

AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol

185 lines 6.57 kB
import { nwc } from "@getalby/sdk"; import { DEFAULT_MAX_PARALLEL_PAYMENTS } from "../common/constants.js"; import { sha256 } from "@noble/hashes/sha2"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import { parseBolt11 } from "../common/bolt11.js"; /** * LightningPaymentManager handles lightning payments using NWC * with a limit on parallel payment operations */ export class LightningPaymentManager { /** * Creates a new LightningPaymentManager * * @param nwcString - NWC connection string * @param maxParallelPayments - Maximum number of parallel payments (default: 5) */ constructor(nwcString, maxParallelPayments = DEFAULT_MAX_PARALLEL_PAYMENTS) { this.activePayments = 0; this.paymentQueue = []; if (!nwcString) { throw new Error("NWC connection string is required"); } this.nwcClient = new nwc.NWCClient({ nostrWalletConnectUrl: nwcString, }); this.maxParallelPayments = maxParallelPayments; } /** * Pays a lightning invoice * * @param invoice - Lightning invoice to pay * @returns Promise resolving to payment preimage */ async payInvoice(invoice) { // Create a promise that will be resolved when the payment is processed return new Promise((resolve, reject) => { // Create a function to process the payment const processPayment = async () => { this.activePayments++; try { // Pay the invoice const paymentResult = await this.nwcClient.payInvoice({ invoice, }); // Resolve with the preimage resolve(paymentResult.preimage); } catch (error) { // Reject with the error reject(error instanceof Error ? error : new Error(String(error))); } finally { // Decrement the active payments counter this.activePayments--; // Process the next payment in the queue if any this.processNextPayment(); } }; // If we can process the payment immediately, do so if (this.activePayments < this.maxParallelPayments) { processPayment(); } else { // Otherwise, add it to the queue this.paymentQueue.push(processPayment); } }); } /** * Processes the next payment in the queue if any * @private */ processNextPayment() { if (this.paymentQueue.length > 0 && this.activePayments < this.maxParallelPayments) { const nextPayment = this.paymentQueue.shift(); if (nextPayment) { nextPayment(); } } } /** * Disposes of resources when the manager is no longer needed */ [Symbol.dispose]() { // Close the NWC client this.nwcClient.close(); } /** * Creates a lightning invoice * * @param amount - Amount in satoshis * @param description - Invoice description * @param expiry - Expiry time in seconds (optional) * @returns Promise resolving to an object containing the invoice and payment hash */ async makeInvoice(amount, description, expiry) { // Convert amount to millisatoshis const amountMsat = amount * 1000; // Create invoice options const invoiceOptions = { amount: amountMsat, description, }; // Add expiry if provided if (expiry) { invoiceOptions.expiry = expiry; } // Create the invoice const invoiceResponse = await this.nwcClient.makeInvoice(invoiceOptions); return { invoice: invoiceResponse.invoice, paymentHash: invoiceResponse.payment_hash, }; } /** * Creates invoices for expert payment * * @param amountSats - Amount in satoshis * @param description - Invoice description * @param expirySec - Expiry time in seconds * @returns Promise resolving to an array of invoices */ async makeInvoices(amountSats, description, expirySec) { const { invoice } = await this.makeInvoice(amountSats, description, expirySec); return [ { method: "lightning", unit: "sat", amount: amountSats, invoice, }, ]; } /** * Verifies a payment using the preimage * * @param paymentHash - Payment hash to verify * @param preimage - Payment preimage * @throws Error if verification fails */ async verifyLightningPayment({ invoice, payment_hash, preimage, }) { if (!invoice && !payment_hash) throw new Error("Either invoice or paymentHash must be provided"); if (!payment_hash) { payment_hash = parseBolt11(invoice).payment_hash; } // Verify that sha256(preimage) === paymentHash const preimageBytes = hexToBytes(preimage); const calculatedHash = bytesToHex(sha256(preimageBytes)); if (calculatedHash !== payment_hash) { throw new Error("Invalid preimage: hash does not match payment hash"); } // Lookup the invoice to verify it's settled const tx = await this.nwcClient.lookupInvoice({ payment_hash, }); if (!tx) { throw new Error("Invoice not found"); } if (!tx.settled_at || tx.settled_at === 0) { throw new Error("Invoice not settled"); } } /** * Verifies a payment * * @param quote - The expert quote containing invoices * @param proof - The payment proof * @throws Error if verification fails */ async verifyPayment(quote, proof) { // Find the lightning invoice const lightningInvoice = quote.invoices.find((inv) => inv.method === "lightning"); if (!lightningInvoice || !lightningInvoice.invoice) { throw new Error("No lightning invoice found in quote"); } // Verify the payment await this.verifyLightningPayment({ invoice: lightningInvoice.invoice, preimage: proof.preimage, }); } } //# sourceMappingURL=LightningPaymentManager.js.map