mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-05-23 10:22:03 +00:00
Basic invoices polling impl to deal with broadcasting zap receipts
This commit is contained in:
parent
c8923191f9
commit
81e064401f
117
src/pages/api/invoices/polling.js
Normal file
117
src/pages/api/invoices/polling.js
Normal file
@ -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' });
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,11 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { finalizeEvent } from 'nostr-tools/pure';
|
|
||||||
import { SimplePool } from 'nostr-tools/pool';
|
|
||||||
import appConfig from "@/config/appConfig";
|
import appConfig from "@/config/appConfig";
|
||||||
import { getLightningAddressByName } from "@/db/models/lightningAddressModels";
|
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 PLEBDEVS_API_KEY = process.env.PLEBDEVS_API_KEY;
|
||||||
const BACKEND_URL = process.env.BACKEND_URL;
|
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) {
|
export default async function handler(req, res) {
|
||||||
// make sure api key is in authorization header
|
// make sure api key is in authorization header
|
||||||
const apiKey = req.headers['authorization'];
|
const apiKey = req.headers['authorization'];
|
||||||
@ -77,45 +57,30 @@ export default async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const invoice = response.data.payment_request;
|
const invoice = response.data.payment_request;
|
||||||
|
const expiry = response.data.expiry;
|
||||||
const paymentHash = Buffer.from(response.data.r_hash, 'base64');
|
const paymentHash = Buffer.from(response.data.r_hash, 'base64');
|
||||||
const paymentHashHex = paymentHash.toString('hex');
|
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) {
|
if (zap_request && foundAddress.allowsNostr) {
|
||||||
const zapRequest = JSON.parse(zap_request);
|
const zapRequest = JSON.parse(zap_request);
|
||||||
const zapReceipt = {
|
const verifyUrl = `${BACKEND_URL}/api/lightning-address/verify/${name}/${paymentHashHex}`;
|
||||||
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)]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start payment polling in the background
|
// Store in Redis with 24-hour expiration
|
||||||
const pollPromise = pollPaymentStatus(BACKEND_URL, name, paymentHashHex);
|
await kv.set(`invoice:${paymentHashHex}`, {
|
||||||
|
verifyUrl,
|
||||||
|
zapRequest,
|
||||||
|
name,
|
||||||
|
invoice,
|
||||||
|
foundAddress,
|
||||||
|
settled: false
|
||||||
|
}, { ex: expiry || 86400 }); // expiry matches invoice expiry
|
||||||
|
|
||||||
// Send the response immediately
|
res.status(200).json({
|
||||||
res.status(200).json({ invoice, payment_hash: paymentHashHex });
|
invoice,
|
||||||
|
payment_hash: paymentHashHex,
|
||||||
// Wait for payment to settle
|
verify_url: verifyUrl
|
||||||
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");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"functions": {
|
"functions": {
|
||||||
"src/pages/api/lightning-address/lnd.js": {
|
"src/pages/api/lightning-address/lnd.js": {
|
||||||
"maxDuration": 300
|
"maxDuration": 120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user