stacker.news/worker/wallet.js

115 lines
4.1 KiB
JavaScript

const serialize = require('../api/resolvers/serial')
const { getInvoice, getPayment, cancelHodlInvoice } = require('ln-service')
const { datePivot } = require('../lib/time')
const walletOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
// TODO this should all be done via websockets
function checkInvoice ({ boss, models, lnd }) {
return async function ({ data: { hash, isHeldSet } }) {
let inv
try {
inv = await getInvoice({ id: hash, lnd })
} catch (err) {
console.log(err)
// on lnd related errors, we manually retry so we don't exponentially backoff
await boss.send('checkInvoice', { hash }, walletOptions)
return
}
console.log(inv)
// check if invoice still exists since HODL invoices get deleted after usage
const dbInv = await models.invoice.findUnique({ where: { hash } })
if (!dbInv) return
const expired = new Date(inv.expires_at) <= new Date()
if (inv.is_confirmed && !inv.is_held) {
// never mark hodl invoices as confirmed here because
// we manually confirm them when we settle them
await serialize(models,
models.$executeRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`)
return boss.send('nip57', { hash })
}
if (inv.is_canceled) {
return serialize(models,
models.invoice.update({
where: {
hash: inv.id
},
data: {
cancelled: true
}
}))
}
if (inv.is_held && !isHeldSet) {
// this is basically confirm_invoice without setting confirmed_at since it's not settled yet
// and without setting the user balance since that's done inside the same tx as the HODL invoice action.
await serialize(models,
models.invoice.update({ where: { hash }, data: { msatsReceived: Number(inv.received_mtokens), isHeld: true } }))
// remember that we already executed this if clause
// (even though the query above is idempotent but imo, this makes the flow more clear)
isHeldSet = true
}
if (!expired) {
// recheck in 5 seconds if the invoice is younger than 5 minutes
// otherwise recheck in 60 seconds
const startAfter = new Date(inv.created_at) > datePivot(new Date(), { minutes: -5 }) ? 5 : 60
await boss.send('checkInvoice', { hash, isHeldSet }, { ...walletOptions, startAfter })
}
if (expired && inv.is_held) {
await cancelHodlInvoice({ id: hash, lnd })
}
}
}
function checkWithdrawal ({ boss, models, lnd }) {
return async function ({ data: { id, hash } }) {
let wdrwl
let notFound = false
try {
wdrwl = await getPayment({ id: hash, lnd })
} catch (err) {
console.log(err)
if (err[1] === 'SentPaymentNotFound') {
notFound = true
} else {
// on lnd related errors, we manually retry so we don't exponentially backoff
await boss.send('checkWithdrawal', { id, hash }, walletOptions)
return
}
}
console.log(wdrwl)
if (wdrwl?.is_confirmed) {
const fee = Number(wdrwl.payment.fee_mtokens)
const paid = Number(wdrwl.payment.mtokens) - fee
await serialize(models, models.$executeRaw`
SELECT confirm_withdrawl(${id}::INTEGER, ${paid}, ${fee})`)
} else if (wdrwl?.is_failed || notFound) {
let status = 'UNKNOWN_FAILURE'
if (wdrwl?.failed.is_insufficient_balance) {
status = 'INSUFFICIENT_BALANCE'
} else if (wdrwl?.failed.is_invalid_payment) {
status = 'INVALID_PAYMENT'
} else if (wdrwl?.failed.is_pathfinding_timeout) {
status = 'PATHFINDING_TIMEOUT'
} else if (wdrwl?.failed.is_route_not_found) {
status = 'ROUTE_NOT_FOUND'
}
await serialize(models, models.$executeRaw`
SELECT reverse_withdrawl(${id}::INTEGER, ${status}::"WithdrawlStatus")`)
} else {
// we need to requeue to check again in 5 seconds
const startAfter = new Date(wdrwl.created_at) > datePivot(new Date(), { minutes: -5 }) ? 5 : 60
await boss.send('checkWithdrawal', { id, hash }, { ...walletOptions, startAfter })
}
}
}
module.exports = { checkInvoice, checkWithdrawal }