mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-23 16:05:24 +00:00
fix zaps on lightning address
This commit is contained in:
parent
8f2935a7dc
commit
2e25beea71
@ -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
23
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
@ -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' });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -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' })
|
||||||
}
|
}
|
21
src/utils/corsMiddleware.js
Normal file
21
src/utils/corsMiddleware.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user