From 30cde2ea388bfa7acc34074b1040987a5c4f5d78 Mon Sep 17 00:00:00 2001 From: keyan Date: Wed, 15 Feb 2023 11:20:26 -0600 Subject: [PATCH] fix nip57? --- api/resolvers/wallet.js | 4 +- pages/api/lnurlp/[username]/pay.js | 12 ++-- .../migration.sql | 2 + .../migration.sql | 36 ++++++++++ prisma/schema.prisma | 1 + worker/nostr.js | 65 +++++++++++-------- worker/wallet.js | 5 +- 7 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 prisma/migrations/20230215152142_desc_on_invoice/migration.sql create mode 100644 prisma/migrations/20230215153049_desc_invoice_function/migration.sql diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 6db4a65e..d6f98100 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -200,7 +200,7 @@ export default { // set expires at to 3 hours into future 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 { const invoice = await createInvoice({ description: user.hideInvoiceDesc ? undefined : description, @@ -211,7 +211,7 @@ export default { const [inv] = await serialize(models, models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request}, - ${expiresAt}, ${amount * 1000}, ${me.id})`) + ${expiresAt}, ${amount * 1000}, ${me.id}, ${description})`) return inv } catch (error) { diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index 5f6157a7..2a66f677 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -1,7 +1,7 @@ import models from '../../../../api/models' import lnd from '../../../../api/lnd' import { createInvoice } from 'ln-service' -import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString } from '../../../../lib/lnurl' +import { lnurlPayDescriptionHashForUser } from '../../../../lib/lnurl' import serialize from '../../../../api/resolvers/serial' import * as secp256k1 from '@noble/secp256k1' import { createHash } from 'crypto' @@ -13,22 +13,22 @@ export default async ({ query: { username, amount, nostr } }, res) => { } try { // if nostr, decode, validate sig, check tags, set description hash - let description, descriptionHash + let description, descriptionHash, noteStr if (nostr) { - const noteStr = decodeURIComponent(nostr) + noteStr = decodeURIComponent(nostr) const note = JSON.parse(noteStr) const hasPTag = note.tags?.filter(t => t[0] === 'p').length >= 1 const hasETag = note.tags?.filter(t => t[0] === 'e').length <= 1 if (await secp256k1.schnorr.verify(note.sig, note.id, note.pubkey) && hasPTag && hasETag) { - description = noteStr + description = user.hideInvoiceDesc ? undefined : 'zap' descriptionHash = createHash('sha256').update(noteStr).digest('hex') } else { res.status(400).json({ status: 'ERROR', reason: 'invalid NIP-57 note' }) return } } else { - description = user.hideInvoiceDesc ? undefined : lnurlPayMetadataString(username) + description = user.hideInvoiceDesc ? undefined : `Funding @${username} on stacker.news` descriptionHash = lnurlPayDescriptionHashForUser(username) } @@ -48,7 +48,7 @@ export default async ({ query: { username, amount, nostr } }, res) => { await serialize(models, 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({ pr: invoice.request, diff --git a/prisma/migrations/20230215152142_desc_on_invoice/migration.sql b/prisma/migrations/20230215152142_desc_on_invoice/migration.sql new file mode 100644 index 00000000..904050a1 --- /dev/null +++ b/prisma/migrations/20230215152142_desc_on_invoice/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Invoice" ADD COLUMN "desc" TEXT; diff --git a/prisma/migrations/20230215153049_desc_invoice_function/migration.sql b/prisma/migrations/20230215153049_desc_invoice_function/migration.sql new file mode 100644 index 00000000..68d3aa08 --- /dev/null +++ b/prisma/migrations/20230215153049_desc_invoice_function/migration.sql @@ -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; +$$; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 13d4680a..2662175c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -442,6 +442,7 @@ model Invoice { hash String @unique bolt11 String + desc String? expiresAt DateTime confirmedAt DateTime? msatsRequested BigInt diff --git a/worker/nostr.js b/worker/nostr.js index 25f1abd9..453ea1fb 100644 --- a/worker/nostr.js +++ b/worker/nostr.js @@ -3,13 +3,18 @@ const { Relay, signId, calculateId, getPublicKey } = require('nostr') const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true } -function nip57 ({ boss, lnd }) { +function nip57 ({ boss, lnd, models }) { return async function ({ data: { hash } }) { console.log('running nip57') - let inv + let inv, lnInv try { - inv = await getInvoice({ id: hash, lnd }) + lnInv = await getInvoice({ id: hash, lnd }) + inv = await models.invoice.findUnique({ + where: { + hash + } + }) } catch (err) { console.log(err) // on lnd related errors, we manually retry which so we don't exponentially backoff @@ -18,7 +23,9 @@ function nip57 ({ boss, lnd }) { } 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 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) @@ -27,45 +34,51 @@ function nip57 ({ boss, lnd }) { if (etag) { tags.push(etag) } - tags.push(['bolt11', inv.request]) - tags.push(['description', inv.description]) - tags.push(['preimage', inv.secret]) + tags.push(['bolt11', lnInv.request]) + tags.push(['description', inv.desc]) + tags.push(['preimage', lnInv.secret]) const e = { kind: 9735, 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: '', tags } e.id = await calculateId(e) e.sig = await signId(process.env.NOSTR_PRIVATE_KEY, e.id) - relays.forEach(r => { - const timeout = 1000 - const relay = Relay(r) + console.log('zap note', e, relays) + await Promise.allSettled( + relays.map(r => new Promise((resolve, reject) => { + const timeout = 1000 + const relay = Relay(r) - function timedout () { - relay.close() - } + function timedout () { + 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', () => { - clearTimeout(timer) - timer = setTimeout(timedout, timeout) - relay.send(['EVENT', e]) - }) + relay.on('open', () => { + clearTimeout(timer) + timer = setTimeout(timedout, timeout) + relay.send(['EVENT', e]) + }) - relay.on('ok', () => { - clearTimeout(timer) - relay.close() - }) - }) + relay.on('ok', () => { + clearTimeout(timer) + relay.close() + console.log('sent zap to', r) + resolve() + }) + }))) } catch (e) { console.log(e) } - console.log('dont running nip57') + console.log('done running nip57') } } diff --git a/worker/wallet.js b/worker/wallet.js index 7cbd505b..918baa64 100644 --- a/worker/wallet.js +++ b/worker/wallet.js @@ -19,10 +19,7 @@ function checkInvoice ({ boss, models, lnd }) { if (inv.is_confirmed) { await serialize(models, models.$executeRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`) - try { - JSON.parse(inv.description) - await boss.send('nip57', { hash }) - } catch {} + await boss.send('nip57', { hash }) } else if (inv.is_canceled) { // mark as cancelled await serialize(models,