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",
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",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"discord.js": "^14.15.3",
"light-bolt11-decoder": "^3.1.1",
"lucide-react": "^0.441.0",
@ -5842,6 +5843,19 @@
"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": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -13542,6 +13556,15 @@
"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": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz",

View File

@ -27,6 +27,7 @@
"bech32": "^2.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"discord.js": "^14.15.3",
"light-bolt11-decoder": "^3.1.1",
"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 [selectedPromotion, setSelectedPromotion] = useState(promotions[0])
const { returnImageProxy } = useImageProxy();

View File

@ -11,7 +11,30 @@ const appConfig = {
"wss://purplerelay.com/",
// "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;

View File

@ -1,24 +1,28 @@
import axios from "axios";
import crypto from "crypto";
import { runMiddleware, corsMiddleware } from "../../../utils/middleware";
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 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') {
const customAddress = appConfig.customLightningAddresses.find(addr => addr.name === slug);
if (customAddress) {
if (queryParams.amount) {
const amount = parseInt(queryParams.amount);
let metadata, metadataString, hash, descriptionHash;
if (queryParams.nostr) {
if (queryParams?.nostr) {
// This is a zap request
const zapRequest = JSON.parse(decodeURIComponent(queryParams.nostr));
console.log("ZAP REQUEST", zapRequest)
// Verify the zap request
if (!verifyEvent(zapRequest)) {
res.status(400).json({ error: 'Invalid zap request' });
@ -37,7 +41,7 @@ export default async function handler(req, res) {
} else {
// This is a regular lnurl-pay request
metadata = [
["text/plain", "PlebDevs LNURL endpoint, CHEERS!"]
["text/plain", `${customAddress.name}'s LNURL endpoint, CHEERS!`]
];
metadataString = JSON.stringify(metadata);
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
const value = amount / 1000;
if (value < 1) {
if (amount < (customAddress.minSendable)) {
res.status(400).json({ error: 'Amount too low' });
return;
} else if (amount > (customAddress.maxSendable || Number.MAX_SAFE_INTEGER)) {
res.status(400).json({ error: 'Amount too high' });
return;
} else {
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 });
} catch (error) {
console.error(error);
@ -61,5 +67,7 @@ export default async function handler(req, res) {
} else {
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 { finalizeEvent } from 'nostr-tools/pure';
import { SimplePool } from 'nostr-tools/pool';
import appConfig from "@/config/appConfig";
const LND_HOST = process.env.LND_HOST;
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) {
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`, {
value: req.body.amount,
description_hash: req.body.description_hash
value_msat: amount,
description_hash: description_hash
}, {
headers: {
'Grpc-Metadata-macaroon': LND_MACAROON,
@ -20,12 +46,13 @@ export default async function handler(req, res) {
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);
if (zap_request && customAddress.allowsNostr) {
console.log("ZAP REQUEST", zap_request)
const zapRequest = JSON.parse(zap_request);
const zapReceipt = {
kind: 9735,
created_at: Math.floor(Date.now() / 1000),
content: '',
content: customAddress.zapMessage || appConfig.defaultZapMessage || '',
tags: [
['p', zapRequest.pubkey],
['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
const pool = new SimplePool();
const relays = zapRequest.tags.find(tag => tag[0] === 'relays')?.[1] || [];
await pool.publish(relays, signedZapReceipt);
const relays = customAddress.defaultRelays || appConfig.defaultRelayUrls || [];
await Promise.any(pool.publish(relays, signedZapReceipt));
}
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 RELAY_PUBKEY = process.env.RELAY_PUBKEY
const ZAP_PUBKEY = process.env.ZAP_PUBKEY
export default async function handler(req, res) {
await runMiddleware(req, res, corsMiddleware);
const { slug } = req.query
if (!slug || slug === 'undefined') {
@ -13,20 +13,24 @@ export default async function handler(req, res) {
return
}
if (slug === 'austin') {
const customAddress = appConfig.customLightningAddresses.find(addr => addr.name === slug)
if (customAddress) {
const metadata = [
["text/plain", "PlebDevs LNURL endpoint, CHEERS!"]
["text/plain", `${customAddress.description}`]
];
res.status(200).json({
callback: `${BACKEND_URL}/api/callback/austin`,
maxSendable: 10000000000,
minSendable: 1000,
callback: `${BACKEND_URL}/api/lightning-address/callback/${customAddress.name}`,
maxSendable: customAddress.maxSendable || 10000000000,
minSendable: customAddress.minSendable || 1000,
metadata: JSON.stringify(metadata),
tag: 'payRequest',
allowsNostr: true,
nostrPubkey: RELAY_PUBKEY
nostrPubkey: ZAP_PUBKEY
})
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);
});
});
}