diff --git a/api/resolvers/serial.js b/api/resolvers/serial.js index 7e512a59..773b8bf0 100644 --- a/api/resolvers/serial.js +++ b/api/resolvers/serial.js @@ -29,12 +29,18 @@ async function serialize (models, call) { if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) { bail(new Error('faucet has been revoked or is exhausted')) } - if (error.message.includes('40001')) { - throw new Error('wallet balance serialization failure - retry again') - } if (error.message.includes('23514')) { bail(new Error('constraint failure')) } + if (error.message.includes('SN_INV_PENDING_LIMIT')) { + bail(new Error('too many pending invoices')) + } + if (error.message.includes('SN_INV_EXCEED_BALANCE')) { + bail(new Error('pending invoices must not cause balance to exceed 1m sats')) + } + if (error.message.includes('40001')) { + throw new Error('wallet balance serialization failure - retry again') + } bail(error) } }, { diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 0ae6ec7c..5eede0cb 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -1,29 +1,11 @@ import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service' -import { UserInputError, AuthenticationError, ForbiddenError } from 'apollo-server-micro' +import { UserInputError, AuthenticationError } from 'apollo-server-micro' import serialize from './serial' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import lnpr from 'bolt11' import { SELECT } from './item' import { lnurlPayDescriptionHash } from '../../lib/lnurl' -const INVOICE_LIMIT = 10 - -export async function belowInvoiceLimit (models, userId) { - // make sure user has not exceeded INVOICE_LIMIT - const count = await models.invoice.count({ - where: { - userId, - expiresAt: { - gt: new Date() - }, - confirmedAt: null, - cancelled: false - } - }) - - return count < INVOICE_LIMIT -} - export async function getInvoice (parent, { id }, { me, models }) { if (!me) { throw new AuthenticationError('you must be logged in') @@ -199,10 +181,6 @@ export default { const user = await models.user.findUnique({ where: { id: me.id } }) - if (!await belowInvoiceLimit(models, me.id)) { - throw new ForbiddenError('too many pending invoices') - } - // 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` diff --git a/pages/api/lnurlp/[username]/index.js b/pages/api/lnurlp/[username]/index.js index 1e0c1b00..db36a44d 100644 --- a/pages/api/lnurlp/[username]/index.js +++ b/pages/api/lnurlp/[username]/index.js @@ -10,7 +10,7 @@ export default async ({ query: { username } }, res) => { 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 minSendable: 1000, // Min amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendable` - maxSendable: Number.MAX_SAFE_INTEGER, + 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 tag: 'payRequest' // Type of LNURL }) diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index 5f89fa31..416a4ea3 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -3,7 +3,6 @@ import lnd from '../../../../api/lnd' import { createInvoice } from 'ln-service' import { lnurlPayDescriptionHashForUser } from '../../../../lib/lnurl' import serialize from '../../../../api/resolvers/serial' -import { belowInvoiceLimit } from '../../../../api/resolvers/wallet' export default async ({ query: { username, amount } }, res) => { const user = await models.user.findUnique({ where: { name: username } }) @@ -15,10 +14,6 @@ export default async ({ query: { username, amount } }, res) => { return res.status(400).json({ status: 'ERROR', reason: 'amount must be >=1000 msats' }) } - if (!await belowInvoiceLimit(models, user.id)) { - return res.status(400).json({ status: 'ERROR', reason: 'too many pending invoices' }) - } - // generate invoice const expiresAt = new Date(new Date().setMinutes(new Date().getMinutes() + 1)) const description = `${amount} msats for @${user.name} on stacker.news` @@ -42,6 +37,6 @@ export default async ({ query: { username, amount } }, res) => { }) } catch (error) { console.log(error) - res.status(400).json({ status: 'ERROR', reason: 'failed to create invoice' }) + res.status(400).json({ status: 'ERROR', reason: error.message }) } } diff --git a/prisma/migrations/20220830183739_create_invoice/migration.sql b/prisma/migrations/20220830183739_create_invoice/migration.sql new file mode 100644 index 00000000..3ab7b2a4 --- /dev/null +++ b/prisma/migrations/20220830183739_create_invoice/migration.sql @@ -0,0 +1,35 @@ +CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, msats_req INTEGER, user_id INTEGER) +RETURNS "Invoice" +LANGUAGE plpgsql +AS $$ +DECLARE + invoice "Invoice"; + limit_reached BOOLEAN; + too_much BOOLEAN; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT count(*) >= 10, sum("msatsRequested")+max(users.msats)+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) + VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc()) 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