diff --git a/next.config.js b/next.config.js index f56648b..3627c63 100644 --- a/next.config.js +++ b/next.config.js @@ -14,6 +14,10 @@ module.exports = removeImports({ source: '/api/cron', destination: '/api/cron', }, + { + source: "/.well-known/nostr.json", + destination: "/api/nip05", + } ]; }, }); \ No newline at end of file diff --git a/src/pages/api/lightning-address/callback/[slug].js b/src/pages/api/lightning-address/callback/[slug].js new file mode 100644 index 0000000..c46cf04 --- /dev/null +++ b/src/pages/api/lightning-address/callback/[slug].js @@ -0,0 +1,65 @@ +import axios from "axios"; +import crypto from "crypto"; +import { runMiddleware, corsMiddleware } from "../../../utils/middleware"; +import { verifyEvent } from 'nostr-tools/pure'; + +const BACKEND_URL = process.env.BACKEND_URL; +const RELAY_PUBKEY = process.env.RELAY_PUBKEY; + +export default async function handler(req, res) { + await runMiddleware(req, res, corsMiddleware); + const { slug, ...queryParams } = req.query; + + if (slug === 'austin') { + if (queryParams.amount) { + const amount = parseInt(queryParams.amount); + let metadata, metadataString, hash, descriptionHash; + + if (queryParams.nostr) { + // This is a zap request + const zapRequest = JSON.parse(decodeURIComponent(queryParams.nostr)); + + // Verify the zap request + if (!verifyEvent(zapRequest)) { + res.status(400).json({ error: 'Invalid zap request' }); + return; + } + + // Validate zap request + if (zapRequest.kind !== 9734) { + res.status(400).json({ error: 'Invalid zap request' }); + return; + } + + metadataString = JSON.stringify(zapRequest); + hash = crypto.createHash('sha256').update(metadataString).digest('hex'); + descriptionHash = Buffer.from(hash, 'hex').toString('base64'); + } else { + // This is a regular lnurl-pay request + metadata = [ + ["text/plain", "PlebDevs LNURL endpoint, CHEERS!"] + ]; + metadataString = JSON.stringify(metadata); + hash = crypto.createHash('sha256').update(metadataString).digest('hex'); + descriptionHash = Buffer.from(hash, 'hex').toString('base64'); + } + + // Convert amount from millisatoshis to satoshis + const value = amount / 1000; + if (value < 1) { + res.status(400).json({ error: 'Amount too low' }); + return; + } else { + try { + const response = await axios.post(`${BACKEND_URL}/api/lnd`, { amount: value, description_hash: descriptionHash }); + res.status(200).json({ pr: response.data }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Failed to generate invoice' }); + } + } + } else { + res.status(400).json({ error: 'Amount not specified' }); + } + } +} \ No newline at end of file diff --git a/src/pages/api/lightning-address/lnd.js b/src/pages/api/lightning-address/lnd.js new file mode 100644 index 0000000..cee0c40 --- /dev/null +++ b/src/pages/api/lightning-address/lnd.js @@ -0,0 +1,50 @@ +import axios from "axios"; +import { finalizeEvent } from 'nostr-tools/pure'; +import { SimplePool } from 'nostr-tools/pool'; + +const LND_HOST = process.env.LND_HOST; +const LND_MACAROON = process.env.LND_MACAROON; +const RELAY_PRIVKEY = process.env.RELAY_PRIVKEY; + +export default async function handler(req, res) { + try { + const response = await axios.post(`https://${LND_HOST}/v1/invoices`, { + value: req.body.amount, + description_hash: req.body.description_hash + }, { + headers: { + 'Grpc-Metadata-macaroon': LND_MACAROON, + } + }); + + const invoice = response.data.payment_request; + + // If this is a zap, publish a zap receipt + if (req.body.zap_request) { + const zapRequest = JSON.parse(req.body.zap_request); + const zapReceipt = { + kind: 9735, + created_at: Math.floor(Date.now() / 1000), + content: '', + tags: [ + ['p', zapRequest.pubkey], + ['e', zapRequest.id], + ['bolt11', invoice], + ['description', JSON.stringify(zapRequest)] + ] + }; + + const signedZapReceipt = finalizeEvent(zapReceipt, RELAY_PRIVKEY); + + // Publish zap receipt to relays + const pool = new SimplePool(); + const relays = zapRequest.tags.find(tag => tag[0] === 'relays')?.[1] || []; + await pool.publish(relays, signedZapReceipt); + } + + res.status(200).json(invoice); + } catch (error) { + console.error('Error (server) fetching data from LND:', error.message); + res.status(500).json({ message: 'Error fetching data' }); + } +} \ No newline at end of file diff --git a/src/pages/api/lightning-address/lnurlp/[slug].js b/src/pages/api/lightning-address/lnurlp/[slug].js new file mode 100644 index 0000000..b29bacd --- /dev/null +++ b/src/pages/api/lightning-address/lnurlp/[slug].js @@ -0,0 +1,32 @@ +import { runMiddleware, corsMiddleware } from "../../../utils/middleware" + +const BACKEND_URL = process.env.BACKEND_URL +const RELAY_PUBKEY = process.env.RELAY_PUBKEY + +export default async function handler(req, res) { + await runMiddleware(req, res, corsMiddleware); + + const { slug } = req.query + + if (!slug || slug === 'undefined') { + res.status(404).json({ error: 'Not found' }) + return + } + + if (slug === 'austin') { + const metadata = [ + ["text/plain", "PlebDevs LNURL endpoint, CHEERS!"] + ]; + + res.status(200).json({ + callback: `${BACKEND_URL}/api/callback/austin`, + maxSendable: 10000000000, + minSendable: 1000, + metadata: JSON.stringify(metadata), + tag: 'payRequest', + allowsNostr: true, + nostrPubkey: RELAY_PUBKEY + }) + return + } +} \ No newline at end of file diff --git a/src/pages/api/nip05.js b/src/pages/api/nip05.js new file mode 100644 index 0000000..0a7b071 --- /dev/null +++ b/src/pages/api/nip05.js @@ -0,0 +1,10 @@ +const nostrData = { + names: { + plebdevs: + "f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741", + }, +}; + +export default async function Nip05(req, res) { + return res.status(200).json(nostrData); +} \ No newline at end of file