move invoice creation outside of interactive tx

This commit is contained in:
keyan 2024-07-01 16:54:34 -05:00
parent 6e8d7ef1b8
commit 0aa5ba4955
1 changed files with 42 additions and 16 deletions

View File

@ -130,9 +130,11 @@ async function performOptimisticAction (actionType, args, context) {
const { models } = context const { models } = context
const action = paidActions[actionType] const action = paidActions[actionType]
context.optimistic = true
context.lndInvoice = await createLndInvoice(actionType, args, context)
return await models.$transaction(async tx => { return await models.$transaction(async tx => {
context.tx = tx context.tx = tx
context.optimistic = true
const invoice = await createDbInvoice(actionType, args, context) const invoice = await createDbInvoice(actionType, args, context)
@ -160,15 +162,20 @@ async function performPessimisticAction (actionType, args, context) {
const invoice = await verifyPayment(context) const invoice = await verifyPayment(context)
args.invoiceId = invoice.id args.invoiceId = invoice.id
// make sure to perform before settling so we don't race with worker to onPaid
const result = await action.perform(args, context)
// XXX this might cause the interactive tx to time out
await settleHodlInvoice({ secret: invoice.preimage, lnd }) await settleHodlInvoice({ secret: invoice.preimage, lnd })
return { return {
result: await action.perform(args, context), result,
paymentMethod: 'PESSIMISTIC' paymentMethod: 'PESSIMISTIC'
} }
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
} else { } else {
// just create the invoice and complete action when it's paid // just create the invoice and complete action when it's paid
context.lndInvoice = await createLndInvoice(actionType, args, context)
return { return {
invoice: await createDbInvoice(actionType, args, context), invoice: await createDbInvoice(actionType, args, context),
paymentMethod: 'PESSIMISTIC' paymentMethod: 'PESSIMISTIC'
@ -201,13 +208,18 @@ export async function retryPaidAction (actionType, args, context) {
throw new Error(`retryPaidAction - missing invoiceId ${actionType}`) throw new Error(`retryPaidAction - missing invoiceId ${actionType}`)
} }
context.optimistic = true
context.user = await models.user.findUnique({ where: { id: me.id } }) context.user = await models.user.findUnique({ where: { id: me.id } })
const { msatsRequested } = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } })
context.cost = BigInt(msatsRequested)
context.lndInvoice = await createLndInvoice(actionType, args, context)
return await models.$transaction(async tx => { return await models.$transaction(async tx => {
context.tx = tx context.tx = tx
context.optimistic = true
// update the old invoice to RETRYING, so that it's not confused with FAILED // update the old invoice to RETRYING, so that it's not confused with FAILED
const { msatsRequested, actionId } = await tx.invoice.update({ const { actionId } = await tx.invoice.update({
where: { where: {
id: invoiceId, id: invoiceId,
actionState: 'FAILED' actionState: 'FAILED'
@ -217,7 +229,6 @@ export async function retryPaidAction (actionType, args, context) {
} }
}) })
context.cost = BigInt(msatsRequested)
context.actionId = actionId context.actionId = actionId
// create a new invoice // create a new invoice
@ -234,13 +245,14 @@ export async function retryPaidAction (actionType, args, context) {
const OPTIMISTIC_INVOICE_EXPIRE = { minutes: 10 } const OPTIMISTIC_INVOICE_EXPIRE = { minutes: 10 }
const PESSIMISTIC_INVOICE_EXPIRE = { minutes: 10 } const PESSIMISTIC_INVOICE_EXPIRE = { minutes: 10 }
async function createDbInvoice (actionType, args, context) { // we seperate the invoice creation into two functions because
const { user, models, tx, lnd, cost, optimistic, actionId } = context // because if lnd is slow, it'll timeout the interactive tx
async function createLndInvoice (actionType, args, context) {
const { user, lnd, cost, optimistic } = context
const action = paidActions[actionType] const action = paidActions[actionType]
const db = tx ?? models const [createLNDInvoice, expirePivot] = optimistic
const [createLNDInvoice, expirePivot, actionState] = optimistic ? [createInvoice, OPTIMISTIC_INVOICE_EXPIRE]
? [createInvoice, OPTIMISTIC_INVOICE_EXPIRE, 'PENDING'] : [createHodlInvoice, PESSIMISTIC_INVOICE_EXPIRE]
: [createHodlInvoice, PESSIMISTIC_INVOICE_EXPIRE, 'PENDING_HELD']
if (cost < 1000n) { if (cost < 1000n) {
// sanity check // sanity check
@ -248,19 +260,33 @@ async function createDbInvoice (actionType, args, context) {
} }
const expiresAt = datePivot(new Date(), expirePivot) const expiresAt = datePivot(new Date(), expirePivot)
const lndInv = await createLNDInvoice({ return await createLNDInvoice({
description: user?.hideInvoiceDesc ? undefined : await action.describe(args, context), description: user?.hideInvoiceDesc ? undefined : await action.describe(args, context),
lnd, lnd,
mtokens: String(cost), mtokens: String(cost),
expires_at: expiresAt expires_at: expiresAt
}) })
}
async function createDbInvoice (actionType, args, context) {
const { user, models, tx, lndInvoice, cost, optimistic, actionId } = context
const db = tx ?? models
const [expirePivot, actionState] = optimistic
? [OPTIMISTIC_INVOICE_EXPIRE, 'PENDING']
: [PESSIMISTIC_INVOICE_EXPIRE, 'PENDING_HELD']
if (cost < 1000n) {
// sanity check
throw new Error('The cost of the action must be at least 1 sat')
}
const expiresAt = datePivot(new Date(), expirePivot)
const invoice = await db.invoice.create({ const invoice = await db.invoice.create({
data: { data: {
hash: lndInv.id, hash: lndInvoice.id,
msatsRequested: cost, msatsRequested: cost,
preimage: optimistic ? undefined : lndInv.secret, preimage: optimistic ? undefined : lndInvoice.secret,
bolt11: lndInv.request, bolt11: lndInvoice.request,
userId: user?.id || USER_ID.anon, userId: user?.id || USER_ID.anon,
actionType, actionType,
actionState, actionState,
@ -273,7 +299,7 @@ async function createDbInvoice (actionType, args, context) {
await db.$executeRaw` await db.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein, priority) INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein, priority)
VALUES ('checkInvoice', VALUES ('checkInvoice',
jsonb_build_object('hash', ${lndInv.id}::TEXT), 21, true, jsonb_build_object('hash', ${lndInvoice.id}::TEXT), 21, true,
${expiresAt}::TIMESTAMP WITH TIME ZONE, ${expiresAt}::TIMESTAMP WITH TIME ZONE,
${expiresAt}::TIMESTAMP WITH TIME ZONE - now() + interval '10m', 100)` ${expiresAt}::TIMESTAMP WITH TIME ZONE - now() + interval '10m', 100)`