Skip to content

Commit

Permalink
enhance lud-18
Browse files Browse the repository at this point in the history
  • Loading branch information
huumn committed Oct 3, 2023
1 parent e80cadd commit df9173c
Show file tree
Hide file tree
Showing 10 changed files with 11 additions and 131 deletions.
2 changes: 1 addition & 1 deletion api/typeDefs/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default gql`
description: String
item: Item
invoiceComment: String
invoicePayerData: String
invoicePayerData: JSONObject
}
type History {
Expand Down
4 changes: 2 additions & 2 deletions components/invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ export function Invoice ({ invoice, onPayment, info, successVerb }) {
<div className='w-100'>
<AccordianItem
header='sender information'
body={<PayerData data={lud18Data} />}
body={<PayerData data={lud18Data} className='text-muted ms-3 mb-3' />}
/>
</div>}
{comment &&
<div className='w-100'>
<AccordianItem
header='sender comments'
body={<span>{comment}</span>}
body={<span className='text-muted ms-3'>{comment}</span>}
/>
</div>}
</>
Expand Down
24 changes: 3 additions & 21 deletions components/payer-data.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={className}>
{header && <small className='fw-bold'>sender information:</small>}
{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 <div key={key}><small>{value.key} ({key})</small></div>
}
return <div key={key}><small>{value} ({key})</small></div>
})}
</div>
Expand Down
19 changes: 0 additions & 19 deletions lib/validate.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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()
})
15 changes: 2 additions & 13 deletions pages/api/lnurlp/[username]/index.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down
27 changes: 3 additions & 24 deletions pages/api/lnurlp/[username]/pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -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` })
Expand Down Expand Up @@ -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))
Expand All @@ -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() })
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
11 changes: 0 additions & 11 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 0 additions & 4 deletions worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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')
}

Expand Down
19 changes: 0 additions & 19 deletions worker/lnurlp-expire.js

This file was deleted.

0 comments on commit df9173c

Please sign in to comment.