fix nip57?
This commit is contained in:
parent
9f2c8d64bc
commit
30cde2ea38
@ -200,7 +200,7 @@ export default {
|
|||||||
|
|
||||||
// set expires at to 3 hours into future
|
// set expires at to 3 hours into future
|
||||||
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
|
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
|
||||||
const description = `${amount} sats for @${user.name} on stacker.news`
|
const description = `Funding @${user.name} on stacker.news`
|
||||||
try {
|
try {
|
||||||
const invoice = await createInvoice({
|
const invoice = await createInvoice({
|
||||||
description: user.hideInvoiceDesc ? undefined : description,
|
description: user.hideInvoiceDesc ? undefined : description,
|
||||||
@ -211,7 +211,7 @@ export default {
|
|||||||
|
|
||||||
const [inv] = await serialize(models,
|
const [inv] = await serialize(models,
|
||||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||||
${expiresAt}, ${amount * 1000}, ${me.id})`)
|
${expiresAt}, ${amount * 1000}, ${me.id}, ${description})`)
|
||||||
|
|
||||||
return inv
|
return inv
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import models from '../../../../api/models'
|
import models from '../../../../api/models'
|
||||||
import lnd from '../../../../api/lnd'
|
import lnd from '../../../../api/lnd'
|
||||||
import { createInvoice } from 'ln-service'
|
import { createInvoice } from 'ln-service'
|
||||||
import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString } from '../../../../lib/lnurl'
|
import { lnurlPayDescriptionHashForUser } from '../../../../lib/lnurl'
|
||||||
import serialize from '../../../../api/resolvers/serial'
|
import serialize from '../../../../api/resolvers/serial'
|
||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
@ -13,22 +13,22 @@ export default async ({ query: { username, amount, nostr } }, res) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// if nostr, decode, validate sig, check tags, set description hash
|
// if nostr, decode, validate sig, check tags, set description hash
|
||||||
let description, descriptionHash
|
let description, descriptionHash, noteStr
|
||||||
if (nostr) {
|
if (nostr) {
|
||||||
const noteStr = decodeURIComponent(nostr)
|
noteStr = decodeURIComponent(nostr)
|
||||||
const note = JSON.parse(noteStr)
|
const note = JSON.parse(noteStr)
|
||||||
const hasPTag = note.tags?.filter(t => t[0] === 'p').length >= 1
|
const hasPTag = note.tags?.filter(t => t[0] === 'p').length >= 1
|
||||||
const hasETag = note.tags?.filter(t => t[0] === 'e').length <= 1
|
const hasETag = note.tags?.filter(t => t[0] === 'e').length <= 1
|
||||||
if (await secp256k1.schnorr.verify(note.sig, note.id, note.pubkey) &&
|
if (await secp256k1.schnorr.verify(note.sig, note.id, note.pubkey) &&
|
||||||
hasPTag && hasETag) {
|
hasPTag && hasETag) {
|
||||||
description = noteStr
|
description = user.hideInvoiceDesc ? undefined : 'zap'
|
||||||
descriptionHash = createHash('sha256').update(noteStr).digest('hex')
|
descriptionHash = createHash('sha256').update(noteStr).digest('hex')
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({ status: 'ERROR', reason: 'invalid NIP-57 note' })
|
res.status(400).json({ status: 'ERROR', reason: 'invalid NIP-57 note' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
description = user.hideInvoiceDesc ? undefined : lnurlPayMetadataString(username)
|
description = user.hideInvoiceDesc ? undefined : `Funding @${username} on stacker.news`
|
||||||
descriptionHash = lnurlPayDescriptionHashForUser(username)
|
descriptionHash = lnurlPayDescriptionHashForUser(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export default async ({ query: { username, amount, nostr } }, res) => {
|
|||||||
|
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||||
${expiresAt}, ${Number(amount)}, ${user.id})`)
|
${expiresAt}, ${Number(amount)}, ${user.id}, ${noteStr || description})`)
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
pr: invoice.request,
|
pr: invoice.request,
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Invoice" ADD COLUMN "desc" TEXT;
|
@ -0,0 +1,36 @@
|
|||||||
|
DROP FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, msats_req BIGINT, user_id INTEGER);
|
||||||
|
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)
|
||||||
|
RETURNS "Invoice"
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
invoice "Invoice";
|
||||||
|
limit_reached BOOLEAN;
|
||||||
|
too_much BOOLEAN;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT count(*) >= 10, coalesce(sum("msatsRequested"),0)+coalesce(max(users.msats), 0)+msats_req > 1000000000 INTO limit_reached, too_much
|
||||||
|
FROM "Invoice"
|
||||||
|
JOIN users on "userId" = users.id
|
||||||
|
WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" is null AND cancelled = false;
|
||||||
|
|
||||||
|
-- prevent more than 10 pending invoices
|
||||||
|
IF limit_reached THEN
|
||||||
|
RAISE EXCEPTION 'SN_INV_PENDING_LIMIT';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- prevent pending invoices + msats from exceeding 1,000,000 sats
|
||||||
|
IF too_much THEN
|
||||||
|
RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc")
|
||||||
|
VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc) 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;
|
||||||
|
$$;
|
@ -442,6 +442,7 @@ model Invoice {
|
|||||||
|
|
||||||
hash String @unique
|
hash String @unique
|
||||||
bolt11 String
|
bolt11 String
|
||||||
|
desc String?
|
||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
confirmedAt DateTime?
|
confirmedAt DateTime?
|
||||||
msatsRequested BigInt
|
msatsRequested BigInt
|
||||||
|
@ -3,13 +3,18 @@ const { Relay, signId, calculateId, getPublicKey } = require('nostr')
|
|||||||
|
|
||||||
const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
|
const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
|
||||||
|
|
||||||
function nip57 ({ boss, lnd }) {
|
function nip57 ({ boss, lnd, models }) {
|
||||||
return async function ({ data: { hash } }) {
|
return async function ({ data: { hash } }) {
|
||||||
console.log('running nip57')
|
console.log('running nip57')
|
||||||
|
|
||||||
let inv
|
let inv, lnInv
|
||||||
try {
|
try {
|
||||||
inv = await getInvoice({ id: hash, lnd })
|
lnInv = await getInvoice({ id: hash, lnd })
|
||||||
|
inv = await models.invoice.findUnique({
|
||||||
|
where: {
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
// on lnd related errors, we manually retry which so we don't exponentially backoff
|
// on lnd related errors, we manually retry which so we don't exponentially backoff
|
||||||
@ -18,7 +23,9 @@ function nip57 ({ boss, lnd }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const desc = JSON.parse(inv.description)
|
// if parsing fails it's not a zap
|
||||||
|
console.log('zapping', inv.desc)
|
||||||
|
const desc = JSON.parse(inv.desc)
|
||||||
const ptag = desc.tags.filter(t => t?.length >= 2 && t[0] === 'p')[0]
|
const ptag = desc.tags.filter(t => t?.length >= 2 && t[0] === 'p')[0]
|
||||||
const etag = desc.tags.filter(t => t?.length >= 2 && t[0] === 'e')[0]
|
const etag = desc.tags.filter(t => t?.length >= 2 && t[0] === 'e')[0]
|
||||||
const relays = desc.tags.find(t => t?.length >= 2 && t[0] === 'relays').slice(1)
|
const relays = desc.tags.find(t => t?.length >= 2 && t[0] === 'relays').slice(1)
|
||||||
@ -27,45 +34,51 @@ function nip57 ({ boss, lnd }) {
|
|||||||
if (etag) {
|
if (etag) {
|
||||||
tags.push(etag)
|
tags.push(etag)
|
||||||
}
|
}
|
||||||
tags.push(['bolt11', inv.request])
|
tags.push(['bolt11', lnInv.request])
|
||||||
tags.push(['description', inv.description])
|
tags.push(['description', inv.desc])
|
||||||
tags.push(['preimage', inv.secret])
|
tags.push(['preimage', lnInv.secret])
|
||||||
|
|
||||||
const e = {
|
const e = {
|
||||||
kind: 9735,
|
kind: 9735,
|
||||||
pubkey: getPublicKey(process.env.NOSTR_PRIVATE_KEY),
|
pubkey: getPublicKey(process.env.NOSTR_PRIVATE_KEY),
|
||||||
created_at: Math.floor(new Date(inv.confirmed_at).getTime() / 1000),
|
created_at: Math.floor(new Date(lnInv.confirmed_at).getTime() / 1000),
|
||||||
content: '',
|
content: '',
|
||||||
tags
|
tags
|
||||||
}
|
}
|
||||||
e.id = await calculateId(e)
|
e.id = await calculateId(e)
|
||||||
e.sig = await signId(process.env.NOSTR_PRIVATE_KEY, e.id)
|
e.sig = await signId(process.env.NOSTR_PRIVATE_KEY, e.id)
|
||||||
|
|
||||||
relays.forEach(r => {
|
console.log('zap note', e, relays)
|
||||||
const timeout = 1000
|
await Promise.allSettled(
|
||||||
const relay = Relay(r)
|
relays.map(r => new Promise((resolve, reject) => {
|
||||||
|
const timeout = 1000
|
||||||
|
const relay = Relay(r)
|
||||||
|
|
||||||
function timedout () {
|
function timedout () {
|
||||||
relay.close()
|
relay.close()
|
||||||
}
|
console.log('failed to send to', r)
|
||||||
|
reject(new Error('relay timeout'))
|
||||||
|
}
|
||||||
|
|
||||||
let timer = setTimeout(timedout, timeout)
|
let timer = setTimeout(timedout, timeout)
|
||||||
|
|
||||||
relay.on('open', () => {
|
relay.on('open', () => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
timer = setTimeout(timedout, timeout)
|
timer = setTimeout(timedout, timeout)
|
||||||
relay.send(['EVENT', e])
|
relay.send(['EVENT', e])
|
||||||
})
|
})
|
||||||
|
|
||||||
relay.on('ok', () => {
|
relay.on('ok', () => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
relay.close()
|
relay.close()
|
||||||
})
|
console.log('sent zap to', r)
|
||||||
})
|
resolve()
|
||||||
|
})
|
||||||
|
})))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
console.log('dont running nip57')
|
console.log('done running nip57')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,10 +19,7 @@ function checkInvoice ({ boss, models, lnd }) {
|
|||||||
if (inv.is_confirmed) {
|
if (inv.is_confirmed) {
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$executeRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`)
|
models.$executeRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`)
|
||||||
try {
|
await boss.send('nip57', { hash })
|
||||||
JSON.parse(inv.description)
|
|
||||||
await boss.send('nip57', { hash })
|
|
||||||
} catch {}
|
|
||||||
} else if (inv.is_canceled) {
|
} else if (inv.is_canceled) {
|
||||||
// mark as cancelled
|
// mark as cancelled
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user