Skip to content

Commit

Permalink
Set item status to FAILED if payment failed
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed May 20, 2024
1 parent 28976ec commit f85d944
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 15 deletions.
8 changes: 7 additions & 1 deletion api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,8 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
const description = 'Creating item on stacker.news'
// TODO: allow users to configure expiration
const expiresAt = datePivot(new Date(), { milliseconds: DEFAULT_INVOICE_TIMEOUT_MS })
const mtokens = err.cost - err.balance
// we don't do partial payments yet, users need to pay full amount via invoice
const mtokens = err.cost

const lndInv = await createInvoice({
description: me.hideInvoiceDesc ? undefined : description,
Expand All @@ -1401,6 +1402,11 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
SELECT * FROM create_invoice(${lndInv.id}, NULL, ${lndInv.request},
${expiresAt}::timestamp, ${mtokens}, ${item.userId}::INTEGER, ${description}, NULL, NULL,
${invLimit}::INTEGER, ${balanceLimit}, 'ITEM'::"ActionType", ${item.id}::INTEGER, ${JSON.stringify(actionData)}::JSONB)`)

// since SubscribeToInvoices does not trigger if an invoice expired, we need a job that handles expired invoices
await tx.$queryRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
VALUES ('finalizeAction', jsonb_build_object('type', 'ITEM', 'id', ${item.id}::INTEGER), 21, true, ${expiresAt})`
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable })

// hmac is not required to submit action again but to allow user to cancel payment
Expand Down
9 changes: 6 additions & 3 deletions api/resolvers/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import assertGofacYourself from './ofac'
import assertApiKeyNotPermitted from './apiKey'
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
import { bolt11Tags } from '@/lib/bolt11'
import { handleActionError } from 'worker/wallet'

export async function getInvoice (parent, { id }, { me, models, lnd }) {
const inv = await models.invoice.findUnique({
Expand Down Expand Up @@ -380,7 +381,8 @@ export default {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
}
await cancelHodlInvoice({ id: hash, lnd })
const inv = await serialize(
const inv = await models.invoice.findUnique({ where: { hash } })
await serialize([
models.invoice.update({
where: {
hash
Expand All @@ -389,9 +391,10 @@ export default {
cancelled: true
}
}),
{ models }
...handleActionError({ data: inv, models })
], { models }
)
return inv
return { ...inv, cancelled: true }
},
dropBolt11: async (parent, { id }, { me, models, lnd }) => {
if (!me) {
Expand Down
4 changes: 0 additions & 4 deletions prisma/migrations/20240518020747_pending_items/migration.sql
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,6 @@ BEGIN
-- if item is pending, user pays missing sats later.
-- all remaining queries will run when invoice was paid and we update the item status.
IF item."status" = 'PENDING'::"Status" THEN
-- here, we immediately deduct as many of the sats that are required for the payment
-- as possible to effectively "lock" them for it. the remainder will be paid via invoice.
-- if the payment fails, we release the locked sats by adding them to the balance again.
UPDATE users SET msats = GREATEST(msats - cost_msats - item.boost, 0) WHERE id = item."userId";
RETURN item;
END IF;

Expand Down
8 changes: 8 additions & 0 deletions worker/action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { handleActionError } from './wallet'

export async function finalizeAction ({ data: { type, id }, models }) {
const queries = handleActionError({ data: { actionType: type, actionId: id }, models })
if (queries.length === 0) return

await models.$transaction(queries)
}
2 changes: 2 additions & 0 deletions worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ofac } from './ofac.js'
import { autoWithdraw } from './autowithdraw.js'
import { saltAndHashEmails } from './saltAndHashEmails.js'
import { remindUser } from './reminder.js'
import { finalizeAction } from './action.js'

const { loadEnvConfig } = nextEnv
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
Expand Down Expand Up @@ -80,6 +81,7 @@ async function work () {

await subscribeToWallet(args)
await boss.work('finalizeHodlInvoice', jobWrapper(finalizeHodlInvoice))
await boss.work('finalizeAction', jobWrapper(finalizeAction))
await boss.work('checkPendingDeposits', jobWrapper(checkPendingDeposits))
await boss.work('checkPendingWithdrawals', jobWrapper(checkPendingWithdrawals))
await boss.work('autoDropBolt11s', jobWrapper(autoDropBolt11s))
Expand Down
30 changes: 23 additions & 7 deletions worker/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ async function subscribeToDeposits (args) {
} else {
// this is a HODL invoice. We need to use SubscribeToInvoice which has is_held transitions
// https://api.lightning.community/api/lnd/invoices/subscribe-single-invoice
// SubscribeToInvoices is only for invoice creation and settlement transitions
// SubscribeToInvoices is only for invoice creation and settlement transitions.
// This means if invoices expire or are cancelled, we will not get notified here.
// https://api.lightning.community/api/lnd/lightning/subscribe-invoices
subscribeToHodlInvoice({ hash: inv.id, ...args })
}
Expand Down Expand Up @@ -165,16 +166,17 @@ async function checkInvoice ({ data: { hash }, boss, models, lnd }) {
}

if (inv.is_canceled) {
return await serialize(
return await serialize([
models.invoice.update({
where: {
hash: inv.id
},
data: {
cancelled: true
}
}), { models }
)
}),
...handleActionError({ data: dbInv, models })
], { models })
}
}

Expand Down Expand Up @@ -363,8 +365,7 @@ async function handleAction ({ data: { msatsReceived, actionType, actionId, acti
// update item status from PENDING to ACTIVE
// and run queries which were skipped during creation

const { credits: locked, cost } = actionData

const { cost } = actionData
const item = await models.item.findUnique({ where: { id: actionId } })

await serialize([
Expand All @@ -382,7 +383,7 @@ async function handleAction ({ data: { msatsReceived, actionType, actionId, acti
},
data: {
msats: {
decrement: cost - msatsReceived - locked
decrement: cost
}
}
}),
Expand All @@ -395,3 +396,18 @@ async function handleAction ({ data: { msatsReceived, actionType, actionId, acti
], { models })
}
}

export function handleActionError ({ data: { actionType, actionId }, models }) {
if (!actionType || !actionId) return []

if (actionType === 'ITEM') {
return [
models.$queryRaw`
UPDATE "Item"
SET status = 'FAILED'
WHERE id = ${actionId} AND status = 'PENDING'`
]
}

return []
}

0 comments on commit f85d944

Please sign in to comment.