fix nip57?

This commit is contained in:
keyan 2023-02-15 11:20:26 -06:00
parent 9f2c8d64bc
commit 30cde2ea38
7 changed files with 87 additions and 38 deletions

View File

@ -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) {

View File

@ -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,

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Invoice" ADD COLUMN "desc" TEXT;

View File

@ -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;
$$;

View File

@ -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

View File

@ -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')
} }
} }

View File

@ -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,