From a41bd516f2d309b667e5a6f9cc3619ca8f04de70 Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Sun, 24 Sep 2023 13:01:50 -0400 Subject: [PATCH 01/14] first pass of LUD-18 support --- lib/validate.js | 22 ++++++++++++++++++++++ pages/api/lnurlp/[username]/index.js | 17 ++++++++++++++++- pages/api/lnurlp/[username]/pay.js | 25 +++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/lib/validate.js b/lib/validate.js index a12913eff..001e16905 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -1,3 +1,4 @@ +import { secp256k1 } from '@noble/curves/secp256k1' import { string, ValidationError, number, object, array, addMethod, boolean } from 'yup' import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES, SUBS_NO_JOBS, MAX_FORWARDS, BOOST_MULT } from './constants' import { URL_REGEXP, WS_REGEXP } from './url' @@ -268,3 +269,24 @@ export const pushSubscriptionSchema = object({ p256dh: string().required('required').trim(), auth: string().required('required').trim() }) + +export const lud18PayerDataSchema = (k1) => object({ + name: string(), + pubkey: string(), + auth: object({ + key: string().required('auth key required'), + k1: string().required('auth k1 required').equals(k1, 'must equal original k1 value'), + sig: string().required('auth sig required') + }) + .test('verify auth signature', auth => { + const { key, k1, sig } = auth + try { + return secp256k1.verify(sig, k1, key) + } catch (err) { + console.log('error caught validating auth signature', err) + return false + } + }), + email: string(), + identifier: string() +}) diff --git a/pages/api/lnurlp/[username]/index.js b/pages/api/lnurlp/[username]/index.js index 35cacc597..2719e45d2 100644 --- a/pages/api/lnurlp/[username]/index.js +++ b/pages/api/lnurlp/[username]/index.js @@ -1,20 +1,35 @@ +import { randomBytes } from 'crypto' import { getPublicKey } from 'nostr' import models from '../../../../api/models' import { lnurlPayMetadataString } from '../../../../lib/lnurl' import { LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' +const generateK1 = () => randomBytes(32).toString('hex') + export default async ({ query: { username } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } + const k1 = generateK1() + return res.status(200).json({ - callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay`, // The URL from LN SERVICE which will accept the pay request parameters + callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay?k1=${k1}`, // The URL from LN SERVICE which will accept the pay request parameters minSendable: 1000, // Min amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendable` maxSendable: 1000000000, metadata: lnurlPayMetadataString(username), // Metadata json which must be presented as raw string here, this is required to pass signature verification at a later step commentAllowed: LNURLP_COMMENT_MAX_LENGTH, // LUD-12 Comments for payRequests https://github.com/lnurl/luds/blob/luds/12.md + payerData: { // LUD-18 payer data for payRequests https://github.com/lnurl/luds/blob/luds/18.md + name: { mandatory: false }, + pubkey: { mandatory: false }, + identifier: { mandatory: false }, + email: { mandatory: false }, + auth: { + mandatory: false, + k1 + } + }, tag: 'payRequest', // Type of LNURL nostrPubkey: process.env.NOSTR_PRIVATE_KEY ? getPublicKey(process.env.NOSTR_PRIVATE_KEY) : undefined, allowsNostr: !!process.env.NOSTR_PRIVATE_KEY diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index 14edbee93..c4b50e71d 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -1,14 +1,15 @@ import models from '../../../../api/models' import lnd from '../../../../api/lnd' import { createInvoice } from 'ln-service' -import { lnurlPayDescriptionHashForUser } from '../../../../lib/lnurl' +import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString, lnurlPayDescriptionHash } from '../../../../lib/lnurl' import serialize from '../../../../api/resolvers/serial' import { schnorr } from '@noble/curves/secp256k1' import { createHash } from 'crypto' import { datePivot } from '../../../../lib/time' import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' +import { ssValidate, lud18PayerDataSchema } from '../../../../lib/validate' -export default async ({ query: { username, amount, nostr, comment } }, res) => { +export default async ({ query: { username, amount, nostr, comment, payerdata: payerData, k1 } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) @@ -45,6 +46,26 @@ export default async ({ query: { username, amount, nostr, comment } }, res) => { return res.status(400).json({ status: 'ERROR', reason: `comment cannot exceed ${LNURLP_COMMENT_MAX_LENGTH} characters in length` }) } + if (payerData) { + let parsedPayerData + try { + parsedPayerData = JSON.parse(payerData) + } catch (err) { + console.error('failed to parse payerdata', err) + return res.status(400).json({ status: 'ERROR', reason: 'Invalid JSON supplied for payerdata parameter' }) + } + + try { + await ssValidate(lud18PayerDataSchema, parsedPayerData, k1) + } catch (err) { + return res.status(400).json({ status: 'ERROR', reason: err.toString() }) + } + + // Update description hash to include the passed payer data + const metadataStr = `${lnurlPayMetadataString(username)}${payerData}` + descriptionHash = lnurlPayDescriptionHash(metadataStr) + } + // generate invoice const expiresAt = datePivot(new Date(), { minutes: 1 }) const invoice = await createInvoice({ From ff8990052bc418727e3be7eb6dc870ba47dd4806 Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Mon, 25 Sep 2023 10:59:20 -0400 Subject: [PATCH 02/14] Various LUD-18 updates * don't cache the well-known response, since it includes randomly generated single use values * validate k1 from well-known response to pay URL * only keep k1's for 10 minutes if they go unused * fix validation logic to make auth object optional --- lib/validate.js | 10 +++++++--- next.config.js | 15 +++++++++------ pages/api/lnurlp/[username]/index.js | 8 ++++++++ pages/api/lnurlp/[username]/pay.js | 16 +++++++++++++++- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/lib/validate.js b/lib/validate.js index 001e16905..f7ca8b55e 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -273,12 +273,16 @@ export const pushSubscriptionSchema = object({ export const lud18PayerDataSchema = (k1) => object({ name: string(), pubkey: string(), - auth: object({ + auth: object().shape({ key: string().required('auth key required'), - k1: string().required('auth k1 required').equals(k1, 'must equal original k1 value'), + k1: string().required('auth k1 required').equals([k1], 'must equal original k1 value'), sig: string().required('auth sig required') }) + .default(undefined) .test('verify auth signature', auth => { + if (!auth) { + return true + } const { key, k1, sig } = auth try { return secp256k1.verify(sig, k1, key) @@ -287,6 +291,6 @@ export const lud18PayerDataSchema = (k1) => object({ return false } }), - email: string(), + email: string().email('bad email address'), identifier: string() }) diff --git a/next.config.js b/next.config.js index 4e44b4a47..78b6d1008 100644 --- a/next.config.js +++ b/next.config.js @@ -13,6 +13,10 @@ const corsHeaders = [ value: 'GET, HEAD, OPTIONS' } ] +const noCacheHeader = { + key: 'Cache-Control', + value: 'no-cache' +} let commitHash if (isProd) { @@ -62,17 +66,15 @@ module.exports = withPlausibleProxy()({ { source: '/.well-known/:slug*', headers: [ - ...corsHeaders + ...corsHeaders, + noCacheHeader ] }, // never cache service worker // https://stackoverflow.com/questions/38843970/service-worker-javascript-update-frequency-every-24-hours/38854905#38854905 { source: '/sw.js', - headers: [{ - key: 'Cache-Control', - value: 'no-cache' - }] + headers: [noCacheHeader] }, { source: '/api/lnauth', @@ -83,7 +85,8 @@ module.exports = withPlausibleProxy()({ { source: '/api/lnurlp/:slug*', headers: [ - ...corsHeaders + ...corsHeaders, + noCacheHeader ] }, { diff --git a/pages/api/lnurlp/[username]/index.js b/pages/api/lnurlp/[username]/index.js index 2719e45d2..9a57012bc 100644 --- a/pages/api/lnurlp/[username]/index.js +++ b/pages/api/lnurlp/[username]/index.js @@ -6,13 +6,21 @@ import { LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' const generateK1 = () => randomBytes(32).toString('hex') +export const k1Cache = new Map() + export default async ({ query: { username } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } + // Generate a random k1, cache it with the requested username for validation upon invoice request const k1 = generateK1() + k1Cache.set(k1, username) + // Invalidate the k1 after 10 minutes, if unused + setTimeout(() => { + k1Cache.delete(k1) + }, 1000 * 60 * 10) return res.status(200).json({ callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay?k1=${k1}`, // The URL from LN SERVICE which will accept the pay request parameters diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index c4b50e71d..eeffd35b4 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -8,12 +8,22 @@ import { createHash } from 'crypto' import { datePivot } from '../../../../lib/time' import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' import { ssValidate, lud18PayerDataSchema } from '../../../../lib/validate' +import { k1Cache } from './index' export default async ({ query: { username, amount, nostr, comment, payerdata: payerData, k1 } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } + if (!k1) { + return res.status(400).json({ status: 'ERROR', reason: `k1 value required` }) + } + if (!k1Cache.has(k1)) { + return res.status(400).json({ status: 'ERROR', reason: `k1 has already been used or expired, request another` }) + } + if (k1Cache.get(k1) !== username) { + return res.status(400).json({ status: 'ERROR', reason: `k1 value is not associated with user @${username}, request another for user @${username}` }) + } try { // if nostr, decode, validate sig, check tags, set description hash let description, descriptionHash, noteStr @@ -49,7 +59,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa if (payerData) { let parsedPayerData try { - parsedPayerData = JSON.parse(payerData) + parsedPayerData = JSON.parse(decodeURIComponent(payerData)) } catch (err) { console.error('failed to parse payerdata', err) return res.status(400).json({ status: 'ERROR', reason: 'Invalid JSON supplied for payerdata parameter' }) @@ -58,6 +68,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa try { await ssValidate(lud18PayerDataSchema, parsedPayerData, k1) } catch (err) { + console.error('error validating payer data', err) return res.status(400).json({ status: 'ERROR', reason: err.toString() }) } @@ -81,6 +92,9 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa ${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description}, ${comment || null}, ${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`) + // delete k1 after it's been used successfully + k1Cache.delete(k1) + return res.status(200).json({ pr: invoice.request, routes: [] From 9dc9ff8994e22b7426ab007a5b9be09d2eecc65c Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 09:44:49 -0400 Subject: [PATCH 03/14] Various LUD18 updates * move k1 cache to database * store payer data in invoice db table * show payer data in invoices on satistics page * show comments and payer data on invoice page --- api/resolvers/wallet.js | 13 +++-- api/typeDefs/wallet.js | 2 + components/invoice.js | 14 +++++- fragments/wallet.js | 3 ++ pages/api/lnurlp/[username]/index.js | 8 +-- pages/api/lnurlp/[username]/pay.js | 14 +++--- pages/satistics.js | 10 ++++ .../migration.sql | 16 ++++++ .../20230928004048_lud18_data/migration.sql | 49 +++++++++++++++++++ prisma/schema.prisma | 12 +++++ 10 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql create mode 100644 prisma/migrations/20230928004048_lud18_data/migration.sql diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 06b04473c..5a9c40385 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -93,6 +93,7 @@ export default { ELSE 'PENDING' END as status, "desc" as description, comment as "invoiceComment", + "lud18Data" as "payerData", 'invoice' as type FROM "Invoice" WHERE "userId" = $1 @@ -109,6 +110,7 @@ export default { COALESCE(status::text, 'PENDING') as status, NULL as description, NULL as "invoiceComment", + NULL as "payerData", 'withdrawal' as type FROM "Withdrawl" WHERE "userId" = $1 @@ -135,6 +137,7 @@ export default { NULL AS status, NULL as description, NULL as "invoiceComment", + NULL as "payerData", 'stacked' AS type FROM "ItemAct" JOIN "Item" ON "ItemAct"."itemId" = "Item".id @@ -148,14 +151,14 @@ export default { queries.push( `(SELECT ('earn' || min("Earn".id)) as id, min("Earn".id) as "factId", NULL as bolt11, created_at as "createdAt", sum(msats), - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'earn' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'earn' as type FROM "Earn" WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2 GROUP BY "userId", created_at)`) queries.push( `(SELECT ('referral' || "ReferralAct".id) as id, "ReferralAct".id as "factId", NULL as bolt11, created_at as "createdAt", msats, - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'referral' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'referral' as type FROM "ReferralAct" WHERE "ReferralAct"."referrerId" = $1 AND "ReferralAct".created_at <= $2)`) } @@ -164,7 +167,7 @@ export default { queries.push( `(SELECT ('spent' || "Item".id) as id, "Item".id as "factId", NULL as bolt11, MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats, - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'spent' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'spent' as type FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE "ItemAct"."userId" = $1 @@ -173,7 +176,7 @@ export default { queries.push( `(SELECT ('donation' || "Donation".id) as id, "Donation".id as "factId", NULL as bolt11, created_at as "createdAt", sats * 1000 as msats, - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'donation' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'donation' as type FROM "Donation" WHERE "userId" = $1 AND created_at <= $2)`) @@ -259,7 +262,7 @@ export default { const [inv] = await serialize(models, models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request}, - ${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description}, NULL, + ${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description}, NULL, NULL, ${invLimit}::INTEGER, ${balanceLimit})`) if (hodlInvoice) await models.invoice.update({ where: { hash: invoice.id }, data: { preimage: invoice.secret } }) diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index e5e9bf9c2..e6b8fd3f2 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -27,6 +27,7 @@ export default gql` satsRequested: Int! nostr: JSONObject comment: String + lud18Data: String hmac: String isHeld: Boolean } @@ -55,6 +56,7 @@ export default gql` description: String item: Item invoiceComment: String + payerData: String } type History { diff --git a/components/invoice.js b/components/invoice.js index 36a505254..234f9e9ab 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -38,7 +38,7 @@ export function Invoice ({ invoice, onPayment, info, successVerb }) { } }, [invoice.confirmedAt, invoice.isHeld, invoice.satsReceived]) - const { nostr } = invoice + const { nostr, comment, lud18Data } = invoice return ( <> @@ -70,6 +70,18 @@ export function Invoice ({ invoice, onPayment, info, successVerb }) { /> : null} + {lud18Data &&
+ {Object.entries(JSON.parse(decodeURIComponent(lud18Data))).map(([key, value]) =>
{value} ({key})
)}} + /> +
} + {comment &&
+ {comment}} + /> +
} ) } diff --git a/fragments/wallet.js b/fragments/wallet.js index 3c1a017e2..01874c0a3 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -14,6 +14,8 @@ export const INVOICE = gql` expiresAt nostr isHeld + comment + lud18Data } }` @@ -45,6 +47,7 @@ export const WALLET_HISTORY = gql` type description invoiceComment + payerData item { ...ItemFullFields } diff --git a/pages/api/lnurlp/[username]/index.js b/pages/api/lnurlp/[username]/index.js index 9a57012bc..d128332d9 100644 --- a/pages/api/lnurlp/[username]/index.js +++ b/pages/api/lnurlp/[username]/index.js @@ -6,8 +6,6 @@ import { LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' const generateK1 = () => randomBytes(32).toString('hex') -export const k1Cache = new Map() - export default async ({ query: { username } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { @@ -16,11 +14,7 @@ export default async ({ query: { username } }, res) => { // Generate a random k1, cache it with the requested username for validation upon invoice request const k1 = generateK1() - k1Cache.set(k1, username) - // Invalidate the k1 after 10 minutes, if unused - setTimeout(() => { - k1Cache.delete(k1) - }, 1000 * 60 * 10) + await models.lnUrlpRequest.create({ data: { k1, userId: user.id } }) return res.status(200).json({ callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay?k1=${k1}`, // The URL from LN SERVICE which will accept the pay request parameters diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index eeffd35b4..59e7ad14b 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -8,7 +8,6 @@ import { createHash } from 'crypto' import { datePivot } from '../../../../lib/time' import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' import { ssValidate, lud18PayerDataSchema } from '../../../../lib/validate' -import { k1Cache } from './index' export default async ({ query: { username, amount, nostr, comment, payerdata: payerData, k1 } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) @@ -18,11 +17,12 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa if (!k1) { return res.status(400).json({ status: 'ERROR', reason: `k1 value required` }) } - if (!k1Cache.has(k1)) { - return res.status(400).json({ status: 'ERROR', reason: `k1 has already been used or expired, request another` }) + const lnUrlpRequest = await models.lnUrlpRequest.findUnique({ where: { k1, userId: user.id } }) + if (!lnUrlpRequest) { + return res.status(400).json({ status: 'ERROR', reason: `k1 has already been used, has expired, or does not exist, request another` }) } - if (k1Cache.get(k1) !== username) { - return res.status(400).json({ status: 'ERROR', reason: `k1 value is not associated with user @${username}, request another for user @${username}` }) + if (datePivot(new Date(lnUrlpRequest.createdAt), { minutes: 10 }) < new Date()) { + return res.status(400).json({ status: 'ERROR', reason: `k1 has expired, request another` }) } try { // if nostr, decode, validate sig, check tags, set description hash @@ -90,10 +90,10 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa await serialize(models, models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request}, ${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description}, - ${comment || null}, ${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`) + ${comment || null}, ${payerData || null}, ${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`) // delete k1 after it's been used successfully - k1Cache.delete(k1) + await models.lnUrlpRequest.delete({ where: { id: lnUrlpRequest.id } }) return res.status(200).json({ pr: invoice.request, diff --git a/pages/satistics.js b/pages/satistics.js index 2d137553f..afbb88fc5 100644 --- a/pages/satistics.js +++ b/pages/satistics.js @@ -112,11 +112,21 @@ function Detail ({ fact }) { try { zap = JSON.parse(fact.description) } catch { } + let payerData = null + if (fact.payerData) { + const parsed = JSON.parse(decodeURIComponent(fact.payerData)) + payerData = ( +
+ sender information: {Object.entries(parsed).map(([key, value]) =>
{value} ({key})
)}
+
+ ) + } return (
{(zap && nostr zap{zap.content && `: ${zap.content}`}) || (fact.description && {fact.description})} + {payerData} {fact.invoiceComment && sender says: {fact.invoiceComment}} {!fact.invoiceComment && !fact.description && no description} diff --git a/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql b/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql new file mode 100644 index 000000000..6f23a4126 --- /dev/null +++ b/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "LnUrlpRequest" ( + "id" SERIAL NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "k1" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "LnUrlpRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "LnUrlpRequest.k1_unique" ON "LnUrlpRequest"("k1"); + +-- AddForeignKey +ALTER TABLE "LnUrlpRequest" ADD CONSTRAINT "LnUrlpRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20230928004048_lud18_data/migration.sql b/prisma/migrations/20230928004048_lud18_data/migration.sql new file mode 100644 index 000000000..dfa194903 --- /dev/null +++ b/prisma/migrations/20230928004048_lud18_data/migration.sql @@ -0,0 +1,49 @@ +-- AlterTable +ALTER TABLE "Invoice" ADD COLUMN "lud18Data" TEXT; + +-- Add lud18 data parameter to invoice creation +CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, + msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, lud18_data TEXT, inv_limit INTEGER, balance_limit_msats BIGINT) +RETURNS "Invoice" +LANGUAGE plpgsql +AS $$ +DECLARE + invoice "Invoice"; + inv_limit_reached BOOLEAN; + balance_limit_reached BOOLEAN; + inv_pending_msats BIGINT; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + -- prevent too many pending invoices + SELECT inv_limit > 0 AND count(*) >= inv_limit, sum("msatsRequested") INTO inv_limit_reached, inv_pending_msats + FROM "Invoice" + WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false; + + IF inv_limit_reached THEN + RAISE EXCEPTION 'SN_INV_PENDING_LIMIT'; + END IF; + + -- prevent pending invoices + msats from exceeding the limit + SELECT balance_limit_msats > 0 AND inv_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached + FROM users + WHERE id = user_id; + + IF balance_limit_reached THEN + RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE'; + END IF; + + -- we good, proceed frens + INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc", comment, "lud18Data") + VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc, comment, lud18_data) RETURNING * INTO invoice; + + INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter) + VALUES ('checkInvoice', jsonb_build_object('hash', hash), 21, true, now() + interval '10 seconds'); + + RETURN invoice; +END; +$$; + +-- make sure old function is gone +DROP FUNCTION IF EXISTS create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, + msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, inv_limit INTEGER, balance_limit_msats BIGINT); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3f9f9d0eb..ace06176f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -94,6 +94,7 @@ model User { hideIsContributor Boolean @default(false) muters Mute[] @relation("muter") muteds Mute[] @relation("muted") + LnUrlpRequest LnUrlpRequest[] @relation("LnUrlpRequests") @@index([createdAt], map: "users.created_at_index") @@index([inviteId], map: "users.inviteId_index") @@ -204,6 +205,16 @@ model LnWith { withdrawalId Int? } +// Very similar structure to LnWith, but serves a different purpose so it gets a separate model +model LnUrlpRequest { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + k1 String @unique(map: "LnUrlpRequest.k1_unique") + userId Int + user User @relation("LnUrlpRequests", fields: [userId], references: [id], onDelete: Cascade) +} + model Invite { id String @id @default(cuid()) createdAt DateTime @default(now()) @map("created_at") @@ -449,6 +460,7 @@ model Invoice { msatsReceived BigInt? desc String? comment String? + lud18Data String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([createdAt], map: "Invoice.created_at_index") From 2fbb93b93aa79c9b6e04b2f0d27b950a4cb8729b Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 09:52:05 -0400 Subject: [PATCH 04/14] Show lud18 data in invoice notification --- components/notifications.js | 1 + fragments/notifications.js | 1 + 2 files changed, 2 insertions(+) diff --git a/components/notifications.js b/components/notifications.js index cf3bef6b4..9513d8f35 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -241,6 +241,7 @@ function InvoicePaid ({ n }) {
{numWithUnits(n.earnedSats, { abbreviate: false })} were deposited in your account {timeSince(new Date(n.sortTime))} + {n.invoice.lud18Data && {n.invoice.lud18Data}} {n.invoice.comment && {n.invoice.comment}}
) diff --git a/fragments/notifications.js b/fragments/notifications.js index 68b2ddbb2..2dcb4f6a8 100644 --- a/fragments/notifications.js +++ b/fragments/notifications.js @@ -100,6 +100,7 @@ export const NOTIFICATIONS = gql` id nostr comment + lud18Data } } } From 138233248c721ba828f666ff81573d14ba672ee5 Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 13:39:50 -0400 Subject: [PATCH 05/14] PayerData component for easier display of info in invoice, notification, wallet history --- components/invoice.js | 5 +++-- components/notifications.js | 5 +++-- components/payer-data.js | 34 ++++++++++++++++++++++++++++++++++ pages/satistics.js | 15 +++------------ 4 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 components/payer-data.js diff --git a/components/invoice.js b/components/invoice.js index 234f9e9ab..f0cb91334 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -11,6 +11,7 @@ import { useMe } from './me' import { useShowModal } from './modal' import { sleep } from '../lib/time' import Countdown from './countdown' +import PayerData from './payer-data' export function Invoice ({ invoice, onPayment, info, successVerb }) { const [expired, setExpired] = useState(new Date(invoice.expiredAt) <= new Date()) @@ -72,8 +73,8 @@ export function Invoice ({ invoice, onPayment, info, successVerb }) {
{lud18Data &&
{Object.entries(JSON.parse(decodeURIComponent(lud18Data))).map(([key, value]) =>
{value} ({key})
)}} + header='sender information' + body={} />
} {comment &&
diff --git a/components/notifications.js b/components/notifications.js index 9513d8f35..015339363 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -25,6 +25,7 @@ import { nostrZapDetails } from '../lib/nostr' import Text from './text' import NostrIcon from '../svgs/nostr.svg' import { numWithUnits } from '../lib/format' +import PayerData from './payer-data' function Notification ({ n, fresh }) { const type = n.__typename @@ -241,8 +242,8 @@ function InvoicePaid ({ n }) {
{numWithUnits(n.earnedSats, { abbreviate: false })} were deposited in your account {timeSince(new Date(n.sortTime))} - {n.invoice.lud18Data && {n.invoice.lud18Data}} - {n.invoice.comment && {n.invoice.comment}} + + {n.invoice.comment && sender says:{n.invoice.comment}}
) } diff --git a/components/payer-data.js b/components/payer-data.js new file mode 100644 index 000000000..eb1f378f1 --- /dev/null +++ b/components/payer-data.js @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; + +export default function PayerData({ data, className, header = false }) { + const supportedPayerData = ['name', 'pubkey', 'email', 'identifier', 'auth'] + const [parsed, setParsed] = useState({}) + const [error, setError] = useState(false) + + useEffect(() => { + setError(false) + try { + setParsed(JSON.parse(decodeURIComponent(data))) + } catch (err) { + console.error('error parsing payer data', err) + setError(true) + } + }, [data]) + + if (!data || error) { + return null + } + return
+ {header && sender information:} + {Object.entries(parsed) + // Don't display unsupported keys + .filter(([key]) => supportedPayerData.includes(key)) + .map(([key, value]) => { + if (key === 'auth') { + // display the auth key, not the whole object + return
{value.key} ({key})
+ } + return
{value} ({key})
+ })} +
+} \ No newline at end of file diff --git a/pages/satistics.js b/pages/satistics.js index afbb88fc5..33e6057d1 100644 --- a/pages/satistics.js +++ b/pages/satistics.js @@ -12,9 +12,9 @@ import { Checkbox, Form } from '../components/form' import { useRouter } from 'next/router' import Item from '../components/item' import { CommentFlat } from '../components/comment' -import { Fragment } from 'react' import ItemJob from '../components/item-job' import PageLoading from '../components/page-loading' +import PayerData from '../components/payer-data' export const getServerSideProps = getGetServerSideProps({ query: WALLET_HISTORY, authRequired: true }) @@ -112,22 +112,13 @@ function Detail ({ fact }) { try { zap = JSON.parse(fact.description) } catch { } - let payerData = null - if (fact.payerData) { - const parsed = JSON.parse(decodeURIComponent(fact.payerData)) - payerData = ( -
- sender information: {Object.entries(parsed).map(([key, value]) =>
{value} ({key})
)}
-
- ) - } return (
{(zap && nostr zap{zap.content && `: ${zap.content}`}) || (fact.description && {fact.description})} - {payerData} - {fact.invoiceComment && sender says: {fact.invoiceComment}} + + {fact.invoiceComment && sender says: {fact.invoiceComment}} {!fact.invoiceComment && !fact.description && no description} From 10e4f59c3bfdbd3fa2a01176df4c85408a7ae8b4 Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 16:05:18 -0400 Subject: [PATCH 06/14] `payerData` -> `invoicePayerData` in fact schema --- api/resolvers/wallet.js | 14 +++++++------- api/typeDefs/wallet.js | 2 +- fragments/wallet.js | 2 +- pages/satistics.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 5a9c40385..a2d80d04e 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -93,7 +93,7 @@ export default { ELSE 'PENDING' END as status, "desc" as description, comment as "invoiceComment", - "lud18Data" as "payerData", + "lud18Data" as "invoicePayerData", 'invoice' as type FROM "Invoice" WHERE "userId" = $1 @@ -110,7 +110,7 @@ export default { COALESCE(status::text, 'PENDING') as status, NULL as description, NULL as "invoiceComment", - NULL as "payerData", + NULL as "invoicePayerData", 'withdrawal' as type FROM "Withdrawl" WHERE "userId" = $1 @@ -137,7 +137,7 @@ export default { NULL AS status, NULL as description, NULL as "invoiceComment", - NULL as "payerData", + NULL as "invoicePayerData", 'stacked' AS type FROM "ItemAct" JOIN "Item" ON "ItemAct"."itemId" = "Item".id @@ -151,14 +151,14 @@ export default { queries.push( `(SELECT ('earn' || min("Earn".id)) as id, min("Earn".id) as "factId", NULL as bolt11, created_at as "createdAt", sum(msats), - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'earn' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "invoicePayerData", 'earn' as type FROM "Earn" WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2 GROUP BY "userId", created_at)`) queries.push( `(SELECT ('referral' || "ReferralAct".id) as id, "ReferralAct".id as "factId", NULL as bolt11, created_at as "createdAt", msats, - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'referral' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "invoicePayerData", 'referral' as type FROM "ReferralAct" WHERE "ReferralAct"."referrerId" = $1 AND "ReferralAct".created_at <= $2)`) } @@ -167,7 +167,7 @@ export default { queries.push( `(SELECT ('spent' || "Item".id) as id, "Item".id as "factId", NULL as bolt11, MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats, - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'spent' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "invoicePayerData", 'spent' as type FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE "ItemAct"."userId" = $1 @@ -176,7 +176,7 @@ export default { queries.push( `(SELECT ('donation' || "Donation".id) as id, "Donation".id as "factId", NULL as bolt11, created_at as "createdAt", sats * 1000 as msats, - 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "payerData", 'donation' as type + 0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", NULL as "invoicePayerData", 'donation' as type FROM "Donation" WHERE "userId" = $1 AND created_at <= $2)`) diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index e6b8fd3f2..69232244f 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -56,7 +56,7 @@ export default gql` description: String item: Item invoiceComment: String - payerData: String + invoicePayerData: String } type History { diff --git a/fragments/wallet.js b/fragments/wallet.js index 01874c0a3..0a5a3d62c 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -47,7 +47,7 @@ export const WALLET_HISTORY = gql` type description invoiceComment - payerData + invoicePayerData item { ...ItemFullFields } diff --git a/pages/satistics.js b/pages/satistics.js index 33e6057d1..7dda3a3db 100644 --- a/pages/satistics.js +++ b/pages/satistics.js @@ -117,7 +117,7 @@ function Detail ({ fact }) { {(zap && nostr zap{zap.content && `: ${zap.content}`}) || (fact.description && {fact.description})} - + {fact.invoiceComment && sender says: {fact.invoiceComment}} {!fact.invoiceComment && !fact.description && no description} From a2c3b626b3182e9b34cc9580aae21ff7795ac12b Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 16:05:42 -0400 Subject: [PATCH 07/14] Merge prisma migrations --- .../migration.sql | 50 +++++++++++++++++++ .../20230928004048_lud18_data/migration.sql | 49 ------------------ 2 files changed, 50 insertions(+), 49 deletions(-) delete mode 100644 prisma/migrations/20230928004048_lud18_data/migration.sql diff --git a/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql b/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql index 6f23a4126..5c9b14c7e 100644 --- a/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql +++ b/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql @@ -14,3 +14,53 @@ CREATE UNIQUE INDEX "LnUrlpRequest.k1_unique" ON "LnUrlpRequest"("k1"); -- AddForeignKey ALTER TABLE "LnUrlpRequest" ADD CONSTRAINT "LnUrlpRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AlterTable +ALTER TABLE "Invoice" ADD COLUMN "lud18Data" TEXT; + +-- Add lud18 data parameter to invoice creation +CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, + msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, lud18_data TEXT, inv_limit INTEGER, balance_limit_msats BIGINT) +RETURNS "Invoice" +LANGUAGE plpgsql +AS $$ +DECLARE + invoice "Invoice"; + inv_limit_reached BOOLEAN; + balance_limit_reached BOOLEAN; + inv_pending_msats BIGINT; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + -- prevent too many pending invoices + SELECT inv_limit > 0 AND count(*) >= inv_limit, sum("msatsRequested") INTO inv_limit_reached, inv_pending_msats + FROM "Invoice" + WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false; + + IF inv_limit_reached THEN + RAISE EXCEPTION 'SN_INV_PENDING_LIMIT'; + END IF; + + -- prevent pending invoices + msats from exceeding the limit + SELECT balance_limit_msats > 0 AND inv_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached + FROM users + WHERE id = user_id; + + IF balance_limit_reached THEN + RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE'; + END IF; + + -- we good, proceed frens + INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc", comment, "lud18Data") + VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc, comment, lud18_data) RETURNING * INTO invoice; + + INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter) + VALUES ('checkInvoice', jsonb_build_object('hash', hash), 21, true, now() + interval '10 seconds'); + + RETURN invoice; +END; +$$; + +-- make sure old function is gone +DROP FUNCTION IF EXISTS create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, + msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, inv_limit INTEGER, balance_limit_msats BIGINT); \ No newline at end of file diff --git a/prisma/migrations/20230928004048_lud18_data/migration.sql b/prisma/migrations/20230928004048_lud18_data/migration.sql deleted file mode 100644 index dfa194903..000000000 --- a/prisma/migrations/20230928004048_lud18_data/migration.sql +++ /dev/null @@ -1,49 +0,0 @@ --- AlterTable -ALTER TABLE "Invoice" ADD COLUMN "lud18Data" TEXT; - --- Add lud18 data parameter to invoice creation -CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, - msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, lud18_data TEXT, inv_limit INTEGER, balance_limit_msats BIGINT) -RETURNS "Invoice" -LANGUAGE plpgsql -AS $$ -DECLARE - invoice "Invoice"; - inv_limit_reached BOOLEAN; - balance_limit_reached BOOLEAN; - inv_pending_msats BIGINT; -BEGIN - PERFORM ASSERT_SERIALIZED(); - - -- prevent too many pending invoices - SELECT inv_limit > 0 AND count(*) >= inv_limit, sum("msatsRequested") INTO inv_limit_reached, inv_pending_msats - FROM "Invoice" - WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false; - - IF inv_limit_reached THEN - RAISE EXCEPTION 'SN_INV_PENDING_LIMIT'; - END IF; - - -- prevent pending invoices + msats from exceeding the limit - SELECT balance_limit_msats > 0 AND inv_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached - FROM users - WHERE id = user_id; - - IF balance_limit_reached THEN - RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE'; - END IF; - - -- we good, proceed frens - INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc", comment, "lud18Data") - VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc, comment, lud18_data) RETURNING * INTO invoice; - - INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter) - VALUES ('checkInvoice', jsonb_build_object('hash', hash), 21, true, now() + interval '10 seconds'); - - RETURN invoice; -END; -$$; - --- make sure old function is gone -DROP FUNCTION IF EXISTS create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, - msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, inv_limit INTEGER, balance_limit_msats BIGINT); \ No newline at end of file From 6fe9a935122b19763b4f934db5d494a4c122b0bf Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 19:43:22 -0400 Subject: [PATCH 08/14] lint fixes --- components/invoice.js | 26 ++++++++++--------- components/payer-data.js | 56 +++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/components/invoice.js b/components/invoice.js index f0cb91334..a5f357bf9 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -71,18 +71,20 @@ export function Invoice ({ invoice, onPayment, info, successVerb }) { /> : null}
- {lud18Data &&
- } - /> -
} - {comment &&
- {comment}} - /> -
} + {lud18Data && +
+ } + /> +
} + {comment && +
+ {comment}} + /> +
} ) } diff --git a/components/payer-data.js b/components/payer-data.js index eb1f378f1..329f7106f 100644 --- a/components/payer-data.js +++ b/components/payer-data.js @@ -1,34 +1,36 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react' -export default function PayerData({ data, className, header = false }) { - const supportedPayerData = ['name', 'pubkey', 'email', 'identifier', 'auth'] - const [parsed, setParsed] = useState({}) - const [error, setError] = useState(false) +export default function PayerData ({ data, className, header = false }) { + const supportedPayerData = ['name', 'pubkey', 'email', 'identifier', 'auth'] + const [parsed, setParsed] = useState({}) + const [error, setError] = useState(false) - useEffect(() => { - setError(false) - try { - setParsed(JSON.parse(decodeURIComponent(data))) - } catch (err) { - console.error('error parsing payer data', err) - setError(true) - } - }, [data]) - - if (!data || error) { - return null + useEffect(() => { + setError(false) + try { + setParsed(JSON.parse(decodeURIComponent(data))) + } catch (err) { + console.error('error parsing payer data', err) + setError(true) } - return
- {header && sender information:} - {Object.entries(parsed) - // Don't display unsupported keys + }, [data]) + + if (!data || error) { + return null + } + return ( +
+ {header && sender information:} + {Object.entries(parsed) + // Don't display unsupported keys .filter(([key]) => supportedPayerData.includes(key)) .map(([key, value]) => { - if (key === 'auth') { - // display the auth key, not the whole object - return
{value.key} ({key})
- } - return
{value} ({key})
+ if (key === 'auth') { + // display the auth key, not the whole object + return
{value.key} ({key})
+ } + return
{value} ({key})
})}
-} \ No newline at end of file + ) +} From 0b7f71113ad9c25539b89011759009e3481aa1f9 Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 19:54:51 -0400 Subject: [PATCH 09/14] worker job to clear out unused lnurlp requests after 30 minutes --- worker/index.js | 4 ++++ worker/lnurlp-expire.js | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 worker/lnurlp-expire.js diff --git a/worker/index.js b/worker/index.js index 7651fab96..3e3083d17 100644 --- a/worker/index.js +++ b/worker/index.js @@ -11,6 +11,7 @@ import { indexItem, indexAllItems } from './search.js' import { timestampItem } from './ots.js' import { computeStreaks, checkStreak } from './streak.js' import { nip57 } from './nostr.js' +import { lnurlpExpire } from './lnurlp-expire.js' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from 'ln-service' import { views, rankViews } from './views.js' @@ -67,6 +68,9 @@ async function work () { await boss.work('views', views(args)) await boss.work('rankViews', rankViews(args)) + // Not a pg-boss job, but still a process to execute on an interval + lnurlpExpire({ models }) + console.log('working jobs') } diff --git a/worker/lnurlp-expire.js b/worker/lnurlp-expire.js new file mode 100644 index 000000000..9906c703b --- /dev/null +++ b/worker/lnurlp-expire.js @@ -0,0 +1,19 @@ +import { datePivot } from '../lib/time.js' + +export function lnurlpExpire ({ models }) { + setInterval(async () => { + try { + const { count } = await models.lnUrlpRequest.deleteMany({ + where: { + createdAt: { + // clear out any requests that are older than 30 minutes + lt: datePivot(new Date(), { minutes: -30 }) + } + } + }) + console.log(`deleted ${count} lnurlp requests`) + } catch (err) { + console.error('error occurred deleting lnurlp requests', err) + } + }, 1000 * 60) // execute once per minute +} \ No newline at end of file From 1a64d849a0031c09486f75043b90e2175aca2911 Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 20:05:30 -0400 Subject: [PATCH 10/14] More linting --- lib/validate.js | 26 +++++++++++++------------- pages/api/lnurlp/[username]/pay.js | 6 +++--- worker/lnurlp-expire.js | 30 +++++++++++++++--------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/validate.js b/lib/validate.js index f7ca8b55e..1033fd1ee 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -278,19 +278,19 @@ export const lud18PayerDataSchema = (k1) => object({ k1: string().required('auth k1 required').equals([k1], 'must equal original k1 value'), sig: string().required('auth sig required') }) - .default(undefined) - .test('verify auth signature', auth => { - if (!auth) { - return true - } - const { key, k1, sig } = auth - try { - return secp256k1.verify(sig, k1, key) - } catch (err) { - console.log('error caught validating auth signature', err) - return false - } - }), + .default(undefined) + .test('verify auth signature', auth => { + if (!auth) { + return true + } + const { key, k1, sig } = auth + try { + return secp256k1.verify(sig, k1, key) + } catch (err) { + console.log('error caught validating auth signature', err) + return false + } + }), email: string().email('bad email address'), identifier: string() }) diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index 59e7ad14b..898a2043d 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -15,14 +15,14 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } if (!k1) { - return res.status(400).json({ status: 'ERROR', reason: `k1 value required` }) + return res.status(400).json({ status: 'ERROR', reason: 'k1 value required' }) } const lnUrlpRequest = await models.lnUrlpRequest.findUnique({ where: { k1, userId: user.id } }) if (!lnUrlpRequest) { - return res.status(400).json({ status: 'ERROR', reason: `k1 has already been used, has expired, or does not exist, request another` }) + return res.status(400).json({ status: 'ERROR', reason: 'k1 has already been used, has expired, or does not exist, request another' }) } if (datePivot(new Date(lnUrlpRequest.createdAt), { minutes: 10 }) < new Date()) { - return res.status(400).json({ status: 'ERROR', reason: `k1 has expired, request another` }) + return res.status(400).json({ status: 'ERROR', reason: 'k1 has expired, request another' }) } try { // if nostr, decode, validate sig, check tags, set description hash diff --git a/worker/lnurlp-expire.js b/worker/lnurlp-expire.js index 9906c703b..b4263c0dd 100644 --- a/worker/lnurlp-expire.js +++ b/worker/lnurlp-expire.js @@ -1,19 +1,19 @@ import { datePivot } from '../lib/time.js' export function lnurlpExpire ({ models }) { - setInterval(async () => { - try { - const { count } = await models.lnUrlpRequest.deleteMany({ - where: { - createdAt: { - // clear out any requests that are older than 30 minutes - lt: datePivot(new Date(), { minutes: -30 }) - } - } - }) - console.log(`deleted ${count} lnurlp requests`) - } catch (err) { - console.error('error occurred deleting lnurlp requests', err) + setInterval(async () => { + try { + const { count } = await models.lnUrlpRequest.deleteMany({ + where: { + createdAt: { + // clear out any requests that are older than 30 minutes + lt: datePivot(new Date(), { minutes: -30 }) + } } - }, 1000 * 60) // execute once per minute -} \ No newline at end of file + }) + console.log(`deleted ${count} lnurlp requests`) + } catch (err) { + console.error('error occurred deleting lnurlp requests', err) + } + }, 1000 * 60) // execute once per minute +} From f1838de872cacbdcfbae61592883328fe76039fa Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Thu, 28 Sep 2023 20:30:59 -0400 Subject: [PATCH 11/14] Move migration to older --- .../migration.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename prisma/migrations/{20230927235126_lud18_lnurlp_requests => 20230927235726_lud18_lnurlp_requests}/migration.sql (100%) diff --git a/prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql b/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql similarity index 100% rename from prisma/migrations/20230927235126_lud18_lnurlp_requests/migration.sql rename to prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql From 3e0f031de33c6f461d95a80af1c810d6d5ec2d53 Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 3 Oct 2023 12:51:38 -0500 Subject: [PATCH 12/14] WIP review --- api/typeDefs/wallet.js | 2 +- next.config.js | 2 +- pages/api/lnurlp/[username]/pay.js | 29 ++++++++++++------- .../migration.sql | 4 +-- prisma/schema.prisma | 2 +- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 69232244f..4324cfb5c 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -27,7 +27,7 @@ export default gql` satsRequested: Int! nostr: JSONObject comment: String - lud18Data: String + lud18Data: JSONObject hmac: String isHeld: Boolean } diff --git a/next.config.js b/next.config.js index 78b6d1008..eee64f4d5 100644 --- a/next.config.js +++ b/next.config.js @@ -15,7 +15,7 @@ const corsHeaders = [ ] const noCacheHeader = { key: 'Cache-Control', - value: 'no-cache' + value: 'no-cache, max-age=0, must-revalidate' } let commitHash diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index 898a2043d..ae1a8ca3a 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -14,16 +14,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } - if (!k1) { - return res.status(400).json({ status: 'ERROR', reason: 'k1 value required' }) - } - const lnUrlpRequest = await models.lnUrlpRequest.findUnique({ where: { k1, userId: user.id } }) - if (!lnUrlpRequest) { - return res.status(400).json({ status: 'ERROR', reason: 'k1 has already been used, has expired, or does not exist, request another' }) - } - if (datePivot(new Date(lnUrlpRequest.createdAt), { minutes: 10 }) < new Date()) { - return res.status(400).json({ status: 'ERROR', reason: 'k1 has expired, request another' }) - } + try { // if nostr, decode, validate sig, check tags, set description hash let description, descriptionHash, noteStr @@ -57,6 +48,24 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa } if (payerData) { + if (!k1) { + return res.status(400).json({ status: 'ERROR', reason: 'k1 value required' }) + } + + const lnUrlpRequest = await models.lnUrlpRequest.findUnique({ + where: { + k1, + userId: user.id, + createdAt: { + gte: datePivot(new Date(), { minutes: -10 }) + } + } + }) + + if (!lnUrlpRequest) { + return res.status(400).json({ status: 'ERROR', reason: 'k1 has already been used, has expired, or does not exist, request another' }) + } + let parsedPayerData try { parsedPayerData = JSON.parse(decodeURIComponent(payerData)) diff --git a/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql b/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql index 5c9b14c7e..a1f9f0537 100644 --- a/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql +++ b/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql @@ -16,11 +16,11 @@ CREATE UNIQUE INDEX "LnUrlpRequest.k1_unique" ON "LnUrlpRequest"("k1"); ALTER TABLE "LnUrlpRequest" ADD CONSTRAINT "LnUrlpRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AlterTable -ALTER TABLE "Invoice" ADD COLUMN "lud18Data" TEXT; +ALTER TABLE "Invoice" ADD COLUMN "lud18Data" JSONB; -- Add lud18 data parameter to invoice creation CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, - msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, lud18_data TEXT, inv_limit INTEGER, balance_limit_msats BIGINT) + msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, lud18_data JSONB, inv_limit INTEGER, balance_limit_msats BIGINT) RETURNS "Invoice" LANGUAGE plpgsql AS $$ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e88b632d1..c2aa451ee 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -461,7 +461,7 @@ model Invoice { msatsReceived BigInt? desc String? comment String? - lud18Data String? + lud18Data Json? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([createdAt], map: "Invoice.created_at_index") From df9173c61b784429714daff14a5beb6eef448110 Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 3 Oct 2023 14:14:20 -0500 Subject: [PATCH 13/14] enhance lud-18 --- api/typeDefs/wallet.js | 2 +- components/invoice.js | 4 +-- components/payer-data.js | 24 +++-------------- lib/validate.js | 19 ------------- pages/api/lnurlp/[username]/index.js | 15 ++--------- pages/api/lnurlp/[username]/pay.js | 27 +++---------------- .../migration.sql | 17 ------------ prisma/schema.prisma | 11 -------- worker/index.js | 4 --- worker/lnurlp-expire.js | 19 ------------- 10 files changed, 11 insertions(+), 131 deletions(-) delete mode 100644 worker/lnurlp-expire.js diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 4324cfb5c..a7d103554 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -56,7 +56,7 @@ export default gql` description: String item: Item invoiceComment: String - invoicePayerData: String + invoicePayerData: JSONObject } type History { diff --git a/components/invoice.js b/components/invoice.js index a5f357bf9..cb9e72183 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -75,14 +75,14 @@ export function Invoice ({ invoice, onPayment, info, successVerb }) {
} + body={} />
} {comment &&
{comment}} + body={{comment}} />
} diff --git a/components/payer-data.js b/components/payer-data.js index 329f7106f..64c8f77bb 100644 --- a/components/payer-data.js +++ b/components/payer-data.js @@ -1,34 +1,16 @@ -import { useEffect, useState } from 'react' - export default function PayerData ({ data, className, header = false }) { - const supportedPayerData = ['name', 'pubkey', 'email', 'identifier', 'auth'] - const [parsed, setParsed] = useState({}) - const [error, setError] = useState(false) - - useEffect(() => { - setError(false) - try { - setParsed(JSON.parse(decodeURIComponent(data))) - } catch (err) { - console.error('error parsing payer data', err) - setError(true) - } - }, [data]) + const supportedPayerData = ['name', 'pubkey', 'email', 'identifier'] - if (!data || error) { + if (!data) { return null } return (
{header && sender information:} - {Object.entries(parsed) + {Object.entries(data) // Don't display unsupported keys .filter(([key]) => supportedPayerData.includes(key)) .map(([key, value]) => { - if (key === 'auth') { - // display the auth key, not the whole object - return
{value.key} ({key})
- } return
{value} ({key})
})}
diff --git a/lib/validate.js b/lib/validate.js index 1033fd1ee..860f3502d 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -1,4 +1,3 @@ -import { secp256k1 } from '@noble/curves/secp256k1' import { string, ValidationError, number, object, array, addMethod, boolean } from 'yup' import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES, SUBS_NO_JOBS, MAX_FORWARDS, BOOST_MULT } from './constants' import { URL_REGEXP, WS_REGEXP } from './url' @@ -273,24 +272,6 @@ export const pushSubscriptionSchema = object({ export const lud18PayerDataSchema = (k1) => object({ name: string(), pubkey: string(), - auth: object().shape({ - key: string().required('auth key required'), - k1: string().required('auth k1 required').equals([k1], 'must equal original k1 value'), - sig: string().required('auth sig required') - }) - .default(undefined) - .test('verify auth signature', auth => { - if (!auth) { - return true - } - const { key, k1, sig } = auth - try { - return secp256k1.verify(sig, k1, key) - } catch (err) { - console.log('error caught validating auth signature', err) - return false - } - }), email: string().email('bad email address'), identifier: string() }) diff --git a/pages/api/lnurlp/[username]/index.js b/pages/api/lnurlp/[username]/index.js index d128332d9..8c413a931 100644 --- a/pages/api/lnurlp/[username]/index.js +++ b/pages/api/lnurlp/[username]/index.js @@ -1,23 +1,16 @@ -import { randomBytes } from 'crypto' import { getPublicKey } from 'nostr' import models from '../../../../api/models' import { lnurlPayMetadataString } from '../../../../lib/lnurl' import { LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' -const generateK1 = () => randomBytes(32).toString('hex') - export default async ({ query: { username } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } - // Generate a random k1, cache it with the requested username for validation upon invoice request - const k1 = generateK1() - await models.lnUrlpRequest.create({ data: { k1, userId: user.id } }) - return res.status(200).json({ - callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay?k1=${k1}`, // The URL from LN SERVICE which will accept the pay request parameters + callback: `${process.env.PUBLIC_URL}/api/lnurlp/${username}/pay`, // The URL from LN SERVICE which will accept the pay request parameters minSendable: 1000, // Min amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendable` maxSendable: 1000000000, metadata: lnurlPayMetadataString(username), // Metadata json which must be presented as raw string here, this is required to pass signature verification at a later step @@ -26,11 +19,7 @@ export default async ({ query: { username } }, res) => { name: { mandatory: false }, pubkey: { mandatory: false }, identifier: { mandatory: false }, - email: { mandatory: false }, - auth: { - mandatory: false, - k1 - } + email: { mandatory: false } }, tag: 'payRequest', // Type of LNURL nostrPubkey: process.env.NOSTR_PRIVATE_KEY ? getPublicKey(process.env.NOSTR_PRIVATE_KEY) : undefined, diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index ae1a8ca3a..66b7501ef 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -9,7 +9,7 @@ import { datePivot } from '../../../../lib/time' import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' import { ssValidate, lud18PayerDataSchema } from '../../../../lib/validate' -export default async ({ query: { username, amount, nostr, comment, payerdata: payerData, k1 } }, res) => { +export default async ({ query: { username, amount, nostr, comment, payerdata: payerData } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) @@ -48,24 +48,6 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa } if (payerData) { - if (!k1) { - return res.status(400).json({ status: 'ERROR', reason: 'k1 value required' }) - } - - const lnUrlpRequest = await models.lnUrlpRequest.findUnique({ - where: { - k1, - userId: user.id, - createdAt: { - gte: datePivot(new Date(), { minutes: -10 }) - } - } - }) - - if (!lnUrlpRequest) { - return res.status(400).json({ status: 'ERROR', reason: 'k1 has already been used, has expired, or does not exist, request another' }) - } - let parsedPayerData try { parsedPayerData = JSON.parse(decodeURIComponent(payerData)) @@ -75,7 +57,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa } try { - await ssValidate(lud18PayerDataSchema, parsedPayerData, k1) + await ssValidate(lud18PayerDataSchema, parsedPayerData) } catch (err) { console.error('error validating payer data', err) return res.status(400).json({ status: 'ERROR', reason: err.toString() }) @@ -99,10 +81,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa await serialize(models, models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request}, ${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description}, - ${comment || null}, ${payerData || null}, ${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`) - - // delete k1 after it's been used successfully - await models.lnUrlpRequest.delete({ where: { id: lnUrlpRequest.id } }) + ${comment || null}, ${payerData || null}::JSONB, ${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`) return res.status(200).json({ pr: invoice.request, diff --git a/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql b/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql index a1f9f0537..7b7986c26 100644 --- a/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql +++ b/prisma/migrations/20230927235726_lud18_lnurlp_requests/migration.sql @@ -1,20 +1,3 @@ --- CreateTable -CREATE TABLE "LnUrlpRequest" ( - "id" SERIAL NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "k1" TEXT NOT NULL, - "userId" INTEGER NOT NULL, - - CONSTRAINT "LnUrlpRequest_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "LnUrlpRequest.k1_unique" ON "LnUrlpRequest"("k1"); - --- AddForeignKey -ALTER TABLE "LnUrlpRequest" ADD CONSTRAINT "LnUrlpRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -- AlterTable ALTER TABLE "Invoice" ADD COLUMN "lud18Data" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 734489e4c..abd50ea46 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -94,7 +94,6 @@ model User { hideIsContributor Boolean @default(false) muters Mute[] @relation("muter") muteds Mute[] @relation("muted") - LnUrlpRequest LnUrlpRequest[] @relation("LnUrlpRequests") @@index([createdAt], map: "users.created_at_index") @@index([inviteId], map: "users.inviteId_index") @@ -205,16 +204,6 @@ model LnWith { withdrawalId Int? } -// Very similar structure to LnWith, but serves a different purpose so it gets a separate model -model LnUrlpRequest { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @default(now()) @updatedAt @map("updated_at") - k1 String @unique(map: "LnUrlpRequest.k1_unique") - userId Int - user User @relation("LnUrlpRequests", fields: [userId], references: [id], onDelete: Cascade) -} - model Invite { id String @id @default(cuid()) createdAt DateTime @default(now()) @map("created_at") diff --git a/worker/index.js b/worker/index.js index d39daa686..55f19a41f 100644 --- a/worker/index.js +++ b/worker/index.js @@ -11,7 +11,6 @@ import { indexItem, indexAllItems } from './search.js' import { timestampItem } from './ots.js' import { computeStreaks, checkStreak } from './streak.js' import { nip57 } from './nostr.js' -import { lnurlpExpire } from './lnurlp-expire.js' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from 'ln-service' import { views, rankViews } from './views.js' @@ -70,9 +69,6 @@ async function work () { await boss.work('rankViews', rankViews(args)) await boss.work('imgproxy', imgproxy(args)) - // Not a pg-boss job, but still a process to execute on an interval - lnurlpExpire({ models }) - console.log('working jobs') } diff --git a/worker/lnurlp-expire.js b/worker/lnurlp-expire.js deleted file mode 100644 index b4263c0dd..000000000 --- a/worker/lnurlp-expire.js +++ /dev/null @@ -1,19 +0,0 @@ -import { datePivot } from '../lib/time.js' - -export function lnurlpExpire ({ models }) { - setInterval(async () => { - try { - const { count } = await models.lnUrlpRequest.deleteMany({ - where: { - createdAt: { - // clear out any requests that are older than 30 minutes - lt: datePivot(new Date(), { minutes: -30 }) - } - } - }) - console.log(`deleted ${count} lnurlp requests`) - } catch (err) { - console.error('error occurred deleting lnurlp requests', err) - } - }, 1000 * 60) // execute once per minute -} From 533850f7e962138c29028ddeb42e9060e0b553e2 Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 3 Oct 2023 14:31:53 -0500 Subject: [PATCH 14/14] refine notification ui --- components/notifications.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/components/notifications.js b/components/notifications.js index 8a8b80815..b440a6cbc 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -25,7 +25,6 @@ import { nostrZapDetails } from '../lib/nostr' import Text from './text' import NostrIcon from '../svgs/nostr.svg' import { numWithUnits } from '../lib/format' -import PayerData from './payer-data' function Notification ({ n, fresh }) { const type = n.__typename @@ -238,12 +237,27 @@ function NostrZap ({ n }) { } function InvoicePaid ({ n }) { + let payerSig + if (n.invoice.lud18Data) { + const { name, identifier, email, pubkey } = n.invoice.lud18Data + const id = identifier || email || pubkey + payerSig = '- ' + if (name) { + payerSig += name + if (id) payerSig += ' \\ ' + } + + if (id) payerSig += id + } return (
{numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} deposited in your account {timeSince(new Date(n.sortTime))} - - {n.invoice.comment && sender says:{n.invoice.comment}} + {n.invoice.comment && + + {n.invoice.comment} + {payerSig} + }
) }