fix zaps on lightning address

This commit is contained in:
austinkelsay 2024-09-18 14:59:04 -05:00
parent 8f2935a7dc
commit 2e25beea71
9 changed files with 139 additions and 27 deletions

View File

@ -17,6 +17,10 @@ module.exports = removeImports({
{ {
source: "/.well-known/nostr.json", source: "/.well-known/nostr.json",
destination: "/api/nip05", destination: "/api/nip05",
},
{
source: '/.well-known/lnurlp/:slug',
destination: '/api/lightning-address/lnurlp/:slug',
} }
]; ];
}, },

23
package-lock.json generated
View File

@ -26,6 +26,7 @@
"bech32": "^2.0.0", "bech32": "^2.0.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cors": "^2.8.5",
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"light-bolt11-decoder": "^3.1.1", "light-bolt11-decoder": "^3.1.1",
"lucide-react": "^0.441.0", "lucide-react": "^0.441.0",
@ -5842,6 +5843,19 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -13542,6 +13556,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vfile": { "node_modules/vfile": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz",

View File

@ -27,6 +27,7 @@
"bech32": "^2.0.0", "bech32": "^2.0.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cors": "^2.8.5",
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"light-bolt11-decoder": "^3.1.1", "light-bolt11-decoder": "^3.1.1",
"lucide-react": "^0.441.0", "lucide-react": "^0.441.0",

View File

@ -57,6 +57,7 @@ const promotions = [
}, },
] ]
// todo bigger ore simple CTA to get users into the content
const InteractivePromotionalCarousel = () => { const InteractivePromotionalCarousel = () => {
const [selectedPromotion, setSelectedPromotion] = useState(promotions[0]) const [selectedPromotion, setSelectedPromotion] = useState(promotions[0])
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();

View File

@ -11,7 +11,30 @@ const appConfig = {
"wss://purplerelay.com/", "wss://purplerelay.com/",
// "wss://relay.devs.tools/" // "wss://relay.devs.tools/"
], ],
authorPubkeys: ["8cb60e215678879cda0bef4d5b3fc1a5c5925d2adb5d8c4fa7b7d03b5f2deaea", "676c02247668d5b18479be3d1a80933044256f3fbd03640a8c234684e641b6d6"] authorPubkeys: ["8cb60e215678879cda0bef4d5b3fc1a5c5925d2adb5d8c4fa7b7d03b5f2deaea", "676c02247668d5b18479be3d1a80933044256f3fbd03640a8c234684e641b6d6"],
customLightningAddresses: [
{
// todo remove need for lowercase
// name will appear as name@plebdevs.com (lowercase)
name: "austin",
// If enabled, zaps are enabled
allowsNostr: true,
// make you're own lud06 metadata description
description: "Austin's Lightning Address",
// millisats
maxSendable: 10000000000,
// millisats
minSendable: 1000,
// Your LND invoice macaroon
invoiceMacaroon: process.env.LND_MACAROON,
// your LND TLS certificate (may be optional depending on your LND configuration)
lndCert: "",
// your LND host (do not include https:// or port)
lndHost: process.env.LND_HOST,
// your LND REST API port (default is 8080)
lndPort: "8080",
},
],
}; };
export default appConfig; export default appConfig;

View File

@ -1,24 +1,28 @@
import axios from "axios"; import axios from "axios";
import crypto from "crypto"; import crypto from "crypto";
import { runMiddleware, corsMiddleware } from "../../../utils/middleware";
import { verifyEvent } from 'nostr-tools/pure'; import { verifyEvent } from 'nostr-tools/pure';
import appConfig from "@/config/appConfig";
import { runMiddleware, corsMiddleware } from "@/utils/corsMiddleware";
const BACKEND_URL = process.env.BACKEND_URL; const BACKEND_URL = process.env.BACKEND_URL;
const RELAY_PUBKEY = process.env.RELAY_PUBKEY;
export default async function handler(req, res) { export default async function handler(req, res) {
await runMiddleware(req, res, corsMiddleware); await runMiddleware(req, res, corsMiddleware);
const { slug, ...queryParams } = req.query; const { slug, ...queryParams } = req.query;
if (slug === 'austin') { const customAddress = appConfig.customLightningAddresses.find(addr => addr.name === slug);
if (customAddress) {
if (queryParams.amount) { if (queryParams.amount) {
const amount = parseInt(queryParams.amount); const amount = parseInt(queryParams.amount);
let metadata, metadataString, hash, descriptionHash; let metadata, metadataString, hash, descriptionHash;
if (queryParams.nostr) { if (queryParams?.nostr) {
// This is a zap request // This is a zap request
const zapRequest = JSON.parse(decodeURIComponent(queryParams.nostr)); const zapRequest = JSON.parse(decodeURIComponent(queryParams.nostr));
console.log("ZAP REQUEST", zapRequest)
// Verify the zap request // Verify the zap request
if (!verifyEvent(zapRequest)) { if (!verifyEvent(zapRequest)) {
res.status(400).json({ error: 'Invalid zap request' }); res.status(400).json({ error: 'Invalid zap request' });
@ -37,7 +41,7 @@ export default async function handler(req, res) {
} else { } else {
// This is a regular lnurl-pay request // This is a regular lnurl-pay request
metadata = [ metadata = [
["text/plain", "PlebDevs LNURL endpoint, CHEERS!"] ["text/plain", `${customAddress.name}'s LNURL endpoint, CHEERS!`]
]; ];
metadataString = JSON.stringify(metadata); metadataString = JSON.stringify(metadata);
hash = crypto.createHash('sha256').update(metadataString).digest('hex'); hash = crypto.createHash('sha256').update(metadataString).digest('hex');
@ -45,13 +49,15 @@ export default async function handler(req, res) {
} }
// Convert amount from millisatoshis to satoshis // Convert amount from millisatoshis to satoshis
const value = amount / 1000; if (amount < (customAddress.minSendable)) {
if (value < 1) {
res.status(400).json({ error: 'Amount too low' }); res.status(400).json({ error: 'Amount too low' });
return; return;
} else if (amount > (customAddress.maxSendable || Number.MAX_SAFE_INTEGER)) {
res.status(400).json({ error: 'Amount too high' });
return;
} else { } else {
try { try {
const response = await axios.post(`${BACKEND_URL}/api/lnd`, { amount: value, description_hash: descriptionHash }); const response = await axios.post(`${BACKEND_URL}/api/lightning-address/lnd`, { amount: amount, description_hash: descriptionHash, name: slug, zap_request: queryParams?.nostr ? queryParams.nostr : null });
res.status(200).json({ pr: response.data }); res.status(200).json({ pr: response.data });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -61,5 +67,7 @@ export default async function handler(req, res) {
} else { } else {
res.status(400).json({ error: 'Amount not specified' }); res.status(400).json({ error: 'Amount not specified' });
} }
} else {
res.status(404).json({ error: 'Lightning address not found' });
} }
} }

View File

@ -1,16 +1,42 @@
import axios from "axios"; import axios from "axios";
import { finalizeEvent } from 'nostr-tools/pure'; import { finalizeEvent } from 'nostr-tools/pure';
import { SimplePool } from 'nostr-tools/pool'; import { SimplePool } from 'nostr-tools/pool';
import appConfig from "@/config/appConfig";
const LND_HOST = process.env.LND_HOST; const LND_HOST = process.env.LND_HOST;
const LND_MACAROON = process.env.LND_MACAROON; const LND_MACAROON = process.env.LND_MACAROON;
const RELAY_PRIVKEY = process.env.RELAY_PRIVKEY; const ZAP_PRIVKEY = process.env.ZAP_PRIVKEY;
export default async function handler(req, res) { export default async function handler(req, res) {
try { try {
const { amount, description_hash, zap_request=null, name } = req.body;
// Find the custom lightning address
const customAddress = appConfig.customLightningAddresses.find(addr => addr.name === name);
if (!customAddress) {
res.status(404).json({ error: 'Lightning address not found' });
return;
}
// Check if amount is within allowed range
const minSendable = customAddress.minSendable || appConfig.defaultMinSendable || 1;
const maxSendable = customAddress.maxSendable || appConfig.defaultMaxSendable || Number.MAX_SAFE_INTEGER;
if (amount < minSendable || amount > maxSendable) {
res.status(400).json({ error: 'Amount out of allowed range' });
return;
}
// Check if the custom address allows zaps
if (zap_request && !customAddress.allowsNostr) {
res.status(400).json({ error: 'Nostr zaps not allowed for this address' });
return;
}
const response = await axios.post(`https://${LND_HOST}/v1/invoices`, { const response = await axios.post(`https://${LND_HOST}/v1/invoices`, {
value: req.body.amount, value_msat: amount,
description_hash: req.body.description_hash description_hash: description_hash
}, { }, {
headers: { headers: {
'Grpc-Metadata-macaroon': LND_MACAROON, 'Grpc-Metadata-macaroon': LND_MACAROON,
@ -20,12 +46,13 @@ export default async function handler(req, res) {
const invoice = response.data.payment_request; const invoice = response.data.payment_request;
// If this is a zap, publish a zap receipt // If this is a zap, publish a zap receipt
if (req.body.zap_request) { if (zap_request && customAddress.allowsNostr) {
const zapRequest = JSON.parse(req.body.zap_request); console.log("ZAP REQUEST", zap_request)
const zapRequest = JSON.parse(zap_request);
const zapReceipt = { const zapReceipt = {
kind: 9735, kind: 9735,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
content: '', content: customAddress.zapMessage || appConfig.defaultZapMessage || '',
tags: [ tags: [
['p', zapRequest.pubkey], ['p', zapRequest.pubkey],
['e', zapRequest.id], ['e', zapRequest.id],
@ -34,12 +61,12 @@ export default async function handler(req, res) {
] ]
}; };
const signedZapReceipt = finalizeEvent(zapReceipt, RELAY_PRIVKEY); const signedZapReceipt = finalizeEvent(zapReceipt, customAddress.relayPrivkey || ZAP_PRIVKEY);
// Publish zap receipt to relays // Publish zap receipt to relays
const pool = new SimplePool(); const pool = new SimplePool();
const relays = zapRequest.tags.find(tag => tag[0] === 'relays')?.[1] || []; const relays = customAddress.defaultRelays || appConfig.defaultRelayUrls || [];
await pool.publish(relays, signedZapReceipt); await Promise.any(pool.publish(relays, signedZapReceipt));
} }
res.status(200).json(invoice); res.status(200).json(invoice);

View File

@ -1,11 +1,11 @@
import { runMiddleware, corsMiddleware } from "../../../utils/middleware" import appConfig from "@/config/appConfig"
import { runMiddleware, corsMiddleware } from "@/utils/corsMiddleware";
const BACKEND_URL = process.env.BACKEND_URL const BACKEND_URL = process.env.BACKEND_URL
const RELAY_PUBKEY = process.env.RELAY_PUBKEY const ZAP_PUBKEY = process.env.ZAP_PUBKEY
export default async function handler(req, res) { export default async function handler(req, res) {
await runMiddleware(req, res, corsMiddleware); await runMiddleware(req, res, corsMiddleware);
const { slug } = req.query const { slug } = req.query
if (!slug || slug === 'undefined') { if (!slug || slug === 'undefined') {
@ -13,20 +13,24 @@ export default async function handler(req, res) {
return return
} }
if (slug === 'austin') { const customAddress = appConfig.customLightningAddresses.find(addr => addr.name === slug)
if (customAddress) {
const metadata = [ const metadata = [
["text/plain", "PlebDevs LNURL endpoint, CHEERS!"] ["text/plain", `${customAddress.description}`]
]; ];
res.status(200).json({ res.status(200).json({
callback: `${BACKEND_URL}/api/callback/austin`, callback: `${BACKEND_URL}/api/lightning-address/callback/${customAddress.name}`,
maxSendable: 10000000000, maxSendable: customAddress.maxSendable || 10000000000,
minSendable: 1000, minSendable: customAddress.minSendable || 1000,
metadata: JSON.stringify(metadata), metadata: JSON.stringify(metadata),
tag: 'payRequest', tag: 'payRequest',
allowsNostr: true, allowsNostr: true,
nostrPubkey: RELAY_PUBKEY nostrPubkey: ZAP_PUBKEY
}) })
return return
} }
res.status(404).json({ error: 'Lightning address not found' })
} }

View File

@ -0,0 +1,21 @@
import Cors from 'cors';
// Initialize the cors middleware
export const corsMiddleware = Cors({
methods: ['GET', 'HEAD', 'POST'],
origin: '*',
});
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
export async function runMiddleware(req, res, middleware) {
return new Promise((resolve, reject) => {
middleware(req, res, (result) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}