constrain invoice quantity and amount
This commit is contained in:
parent
065cf284b3
commit
0ad886ffc0
|
@ -29,12 +29,18 @@ async function serialize (models, call) {
|
||||||
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
|
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
|
||||||
bail(new Error('faucet has been revoked or is 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')) {
|
if (error.message.includes('23514')) {
|
||||||
bail(new Error('constraint failure'))
|
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)
|
bail(error)
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -1,29 +1,11 @@
|
||||||
import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service'
|
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 serialize from './serial'
|
||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||||
import lnpr from 'bolt11'
|
import lnpr from 'bolt11'
|
||||||
import { SELECT } from './item'
|
import { SELECT } from './item'
|
||||||
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
|
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 }) {
|
export async function getInvoice (parent, { id }, { me, models }) {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new AuthenticationError('you must be logged in')
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
@ -199,10 +181,6 @@ export default {
|
||||||
|
|
||||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
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
|
// 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 = `${amount} sats for @${user.name} on stacker.news`
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default async ({ query: { username } }, res) => {
|
||||||
return res.status(200).json({
|
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
|
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`
|
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
|
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
|
tag: 'payRequest' // Type of LNURL
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,6 @@ import lnd from '../../../../api/lnd'
|
||||||
import { createInvoice } from 'ln-service'
|
import { createInvoice } from 'ln-service'
|
||||||
import { lnurlPayDescriptionHashForUser } from '../../../../lib/lnurl'
|
import { lnurlPayDescriptionHashForUser } from '../../../../lib/lnurl'
|
||||||
import serialize from '../../../../api/resolvers/serial'
|
import serialize from '../../../../api/resolvers/serial'
|
||||||
import { belowInvoiceLimit } from '../../../../api/resolvers/wallet'
|
|
||||||
|
|
||||||
export default async ({ query: { username, amount } }, res) => {
|
export default async ({ query: { username, amount } }, res) => {
|
||||||
const user = await models.user.findUnique({ where: { name: username } })
|
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' })
|
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
|
// generate invoice
|
||||||
const expiresAt = new Date(new Date().setMinutes(new Date().getMinutes() + 1))
|
const expiresAt = new Date(new Date().setMinutes(new Date().getMinutes() + 1))
|
||||||
const description = `${amount} msats for @${user.name} on stacker.news`
|
const description = `${amount} msats for @${user.name} on stacker.news`
|
||||||
|
@ -42,6 +37,6 @@ export default async ({ query: { username, amount } }, res) => {
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
res.status(400).json({ status: 'ERROR', reason: 'failed to create invoice' })
|
res.status(400).json({ status: 'ERROR', reason: error.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
$$;
|
Loading…
Reference in New Issue