142 lines
5.0 KiB
JavaScript
142 lines
5.0 KiB
JavaScript
import { GraphQLError } from 'graphql'
|
|
import retry from 'async-retry'
|
|
import Prisma from '@prisma/client'
|
|
import { settleHodlInvoice } from 'ln-service'
|
|
import { createHmac } from './wallet'
|
|
import { msatsToSats, numWithUnits } from '../../lib/format'
|
|
import { BALANCE_LIMIT_MSATS } from '../../lib/constants'
|
|
|
|
export default async function serialize (models, ...calls) {
|
|
return await retry(async bail => {
|
|
try {
|
|
const [, ...result] = await models.$transaction(
|
|
[models.$executeRaw`SELECT ASSERT_SERIALIZED()`, ...calls],
|
|
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable })
|
|
return calls.length > 1 ? result : result[0]
|
|
} catch (error) {
|
|
console.log(error)
|
|
// two cases where we get insufficient funds:
|
|
// 1. plpgsql function raises
|
|
// 2. constraint violation via a prisma call
|
|
// XXX prisma does not provide a way to distinguish these cases so we
|
|
// have to check the error message
|
|
if (error.message.includes('SN_INSUFFICIENT_FUNDS') ||
|
|
error.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
|
|
bail(new GraphQLError('insufficient funds', { extensions: { code: 'BAD_INPUT' } }))
|
|
}
|
|
if (error.message.includes('SN_NOT_SERIALIZABLE')) {
|
|
bail(new Error('wallet balance transaction is not serializable'))
|
|
}
|
|
if (error.message.includes('SN_CONFIRMED_WITHDRAWL_EXISTS')) {
|
|
bail(new Error('withdrawal invoice already confirmed (to withdraw again create a new invoice)'))
|
|
}
|
|
if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) {
|
|
bail(new Error('withdrawal invoice exists and is pending'))
|
|
}
|
|
if (error.message.includes('SN_INELIGIBLE')) {
|
|
bail(new Error('user ineligible for gift'))
|
|
}
|
|
if (error.message.includes('SN_UNSUPPORTED')) {
|
|
bail(new Error('unsupported action'))
|
|
}
|
|
if (error.message.includes('SN_DUPLICATE')) {
|
|
bail(new Error('duplicate not allowed'))
|
|
}
|
|
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
|
|
bail(new Error('faucet has been revoked or is exhausted'))
|
|
}
|
|
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 ${numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))}`))
|
|
}
|
|
if (error.message.includes('40001') || error.code === 'P2034') {
|
|
throw new Error('wallet balance serialization failure - try again')
|
|
}
|
|
if (error.message.includes('23514') || ['P2002', 'P2003', 'P2004'].includes(error.code)) {
|
|
bail(new Error('constraint failure'))
|
|
}
|
|
bail(error)
|
|
}
|
|
}, {
|
|
minTimeout: 10,
|
|
maxTimeout: 100,
|
|
retries: 10
|
|
})
|
|
}
|
|
|
|
export async function serializeInvoicable (query, { models, lnd, hash, hmac, me, enforceFee }) {
|
|
if (!me && !hash) {
|
|
throw new Error('you must be logged in or pay')
|
|
}
|
|
|
|
let trx = Array.isArray(query) ? query : [query]
|
|
|
|
let invoice
|
|
if (hash) {
|
|
invoice = await checkInvoice(models, hash, hmac, enforceFee)
|
|
trx = [
|
|
models.$executeRaw`SELECT confirm_invoice(${hash}, ${invoice.msatsReceived})`,
|
|
...trx
|
|
]
|
|
}
|
|
|
|
let results = await serialize(models, ...trx)
|
|
|
|
if (hash) {
|
|
if (invoice?.isHeld) { await settleHodlInvoice({ secret: invoice.preimage, lnd }) }
|
|
// remove first element since that is the confirmed invoice
|
|
[, ...results] = results
|
|
}
|
|
|
|
// if there is only one result, return it directly, else the array
|
|
results = results.flat(2)
|
|
return results.length > 1 ? results : results[0]
|
|
}
|
|
|
|
export async function checkInvoice (models, hash, hmac, fee) {
|
|
if (!hash) {
|
|
throw new GraphQLError('hash required', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
if (!hmac) {
|
|
throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
const hmac2 = createHmac(hash)
|
|
if (hmac !== hmac2) {
|
|
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
|
|
}
|
|
|
|
const invoice = await models.invoice.findUnique({
|
|
where: { hash },
|
|
include: {
|
|
user: true
|
|
}
|
|
})
|
|
|
|
if (!invoice) {
|
|
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
|
|
const expired = new Date(invoice.expiresAt) <= new Date()
|
|
if (expired) {
|
|
throw new GraphQLError('invoice expired', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
if (invoice.confirmedAt) {
|
|
throw new GraphQLError('invoice already used', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
|
|
if (invoice.cancelled) {
|
|
throw new GraphQLError('invoice was canceled', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
|
|
if (!invoice.msatsReceived) {
|
|
throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
if (fee && msatsToSats(invoice.msatsReceived) < fee) {
|
|
throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
|
|
}
|
|
|
|
return invoice
|
|
}
|