diff --git a/src/pages/api/invoices/polling.js b/src/pages/api/invoices/polling.js new file mode 100644 index 0000000..76440dd --- /dev/null +++ b/src/pages/api/invoices/polling.js @@ -0,0 +1,117 @@ +import axios from "axios"; +import { kv } from '@vercel/kv'; +import { broadcastToRelays } from "@/utils/nostr"; + +const PLEBDEVS_API_KEY = process.env.PLEBDEVS_API_KEY; + +export default async function handler(req, res) { + // Verify API key + const apiKey = req.headers['authorization']; + if (!apiKey || apiKey !== PLEBDEVS_API_KEY) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + try { + // Get all invoice keys from Redis + const keys = await kv.keys('invoice:*'); + const results = { + processed: 0, + settled: 0, + expired: 0, + errors: 0, + pending: 0 + }; + + // Process each invoice + for (const key of keys) { + try { + const invoiceData = await kv.get(key); + if (!invoiceData) continue; + + const { name, foundAddress, zapRequest, settled } = invoiceData; + const paymentHash = key.replace('invoice:', ''); + + // Skip if already settled + if (settled) { + await kv.del(key); + continue; + } + + // Check payment status + const response = await axios.get( + `https://${foundAddress.lndHost}:${foundAddress.lndPort}/v1/invoice/${paymentHash}`, + { + headers: { + 'Grpc-Metadata-macaroon': foundAddress.invoiceMacaroon, + } + } + ); + + if (!response.data) { + results.errors++; + continue; + } + + // Handle expired invoices + if (response.data.state === "EXPIRED" || response.data.state === "CANCELED") { + await kv.del(key); + results.expired++; + continue; + } + + // Handle pending invoices + if (response.data.state === "OPEN") { + results.pending++; + continue; + } + + // Handle settled invoices + if (response.data.state === "SETTLED" && !settled) { + try { + const preimage = Buffer.from(response.data.r_preimage, 'base64').toString('hex'); + + // Parse and prepare zap receipt + const parsedZapRequest = JSON.parse(zapRequest); + const zapReceipt = { + kind: 9735, + created_at: Math.floor(Date.now() / 1000), + content: "", + tags: [ + ["p", parsedZapRequest.tags.find(t => t[0] === "p")[1]], + ["bolt11", response.data.payment_request], + ["description", parsedZapRequest.content], + ["preimage", preimage], + ["request", JSON.stringify(parsedZapRequest)] + ] + }; + + // Broadcast zap receipt to relays + await broadcastToRelays(zapReceipt, foundAddress.nostrPrivateKey); + + console.log(`Broadcasted zap receipt for ${name} (${paymentHash})`, zapReceipt); + + // Delete from Redis after successful broadcast + await kv.del(key); + results.settled++; + } catch (broadcastError) { + console.error('Error broadcasting zap receipt:', broadcastError); + // Keep in Redis for retry if broadcast fails + await kv.set(key, { ...invoiceData, settled: true }, { ex: 3600 }); + results.errors++; + } + } + + results.processed++; + } catch (error) { + console.error('Error processing invoice:', error); + results.errors++; + } + } + + res.status(200).json(results); + } catch (error) { + console.error('Error in polling endpoint:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/src/pages/api/lightning-address/lnd.js b/src/pages/api/lightning-address/lnd.js index cc60554..0a39040 100644 --- a/src/pages/api/lightning-address/lnd.js +++ b/src/pages/api/lightning-address/lnd.js @@ -1,31 +1,11 @@ import axios from "axios"; -import { finalizeEvent } from 'nostr-tools/pure'; -import { SimplePool } from 'nostr-tools/pool'; import appConfig from "@/config/appConfig"; import { getLightningAddressByName } from "@/db/models/lightningAddressModels"; +import { kv } from '@vercel/kv'; -const ZAP_PRIVKEY = process.env.ZAP_PRIVKEY; const PLEBDEVS_API_KEY = process.env.PLEBDEVS_API_KEY; const BACKEND_URL = process.env.BACKEND_URL; -async function pollPaymentStatus(baseUrl, name, paymentHash, maxAttempts = 300, interval = 1000) { - for (let i = 0; i < maxAttempts; i++) { - try { - const response = await axios.get(`${baseUrl}/api/lightning-address/verify/${name}/${paymentHash}`); - console.log(`Polling payment status for ${name}... (${i}/${maxAttempts}), response:`, response.data); - - if (response.data.status === "OK" && response.data.settled) { - return true; - } - - await new Promise(resolve => setTimeout(resolve, interval)); - } catch (error) { - console.error('Error polling payment status:', error.message); - } - } - return false; -} - export default async function handler(req, res) { // make sure api key is in authorization header const apiKey = req.headers['authorization']; @@ -77,45 +57,30 @@ export default async function handler(req, res) { }); const invoice = response.data.payment_request; + const expiry = response.data.expiry; const paymentHash = Buffer.from(response.data.r_hash, 'base64'); const paymentHashHex = paymentHash.toString('hex'); - // If this is a zap, wait for payment and then publish a zap receipt + // If this is a zap, store verification URL and zap request in Redis if (zap_request && foundAddress.allowsNostr) { const zapRequest = JSON.parse(zap_request); - const zapReceipt = { - kind: 9735, - created_at: Math.floor(Date.now() / 1000), - content: foundAddress.zapMessage || appConfig.defaultZapMessage || '', - tags: [ - ['p', zapRequest.pubkey], - ['e', zapRequest.id], - ['bolt11', invoice], - ['description', JSON.stringify(zapRequest)] - ] - }; + const verifyUrl = `${BACKEND_URL}/api/lightning-address/verify/${name}/${paymentHashHex}`; + + // Store in Redis with 24-hour expiration + await kv.set(`invoice:${paymentHashHex}`, { + verifyUrl, + zapRequest, + name, + invoice, + foundAddress, + settled: false + }, { ex: expiry || 86400 }); // expiry matches invoice expiry - // Start payment polling in the background - const pollPromise = pollPaymentStatus(BACKEND_URL, name, paymentHashHex); - - // Send the response immediately - res.status(200).json({ invoice, payment_hash: paymentHashHex }); - - // Wait for payment to settle - const isSettled = await pollPromise; - console.log("Payment settled??", isSettled); - - if (isSettled) { - const signedZapReceipt = finalizeEvent(zapReceipt, foundAddress.relayPrivkey || ZAP_PRIVKEY); - - // Publish zap receipt to relays - const pool = new SimplePool(); - const relays = foundAddress.defaultRelays || appConfig.defaultRelayUrls || []; - await Promise.any(pool.publish(relays, signedZapReceipt)); - console.log("ZAP RECEIPT PUBLISHED", signedZapReceipt); - } else { - console.log("Payment not settled after 60 seconds, skipping zap receipt"); - } + res.status(200).json({ + invoice, + payment_hash: paymentHashHex, + verify_url: verifyUrl + }); return; } diff --git a/vercel.json b/vercel.json index 3aff2eb..d00a404 100644 --- a/vercel.json +++ b/vercel.json @@ -8,7 +8,7 @@ ], "functions": { "src/pages/api/lightning-address/lnd.js": { - "maxDuration": 300 + "maxDuration": 120 } } }