stacker.news/worker/autowithdraw.js

147 lines
4.2 KiB
JavaScript

import { authenticatedLndGrpc, createInvoice as lndCreateInvoice, getIdentity, decodePaymentRequest } from 'ln-service'
import { msatsToSats, satsToMsats } from '@/lib/format'
// import { datePivot } from '@/lib/time'
import { createWithdrawal, /* sendToLnAddr, */ addWalletLog, SERVER_WALLET_DEFS } from '@/api/resolvers/wallet'
// import { createInvoice as createInvoiceCLN } from '@/lib/cln'
export async function autoWithdraw ({ data: { id }, models, lnd }) {
const user = await models.user.findUnique({ where: { id } })
if (user.autoWithdrawThreshold === null || user.autoWithdrawMaxFeePercent === null) return
const threshold = satsToMsats(user.autoWithdrawThreshold)
const excess = Number(user.msats - threshold)
// excess must be greater than 10% of threshold
if (excess < Number(threshold) * 0.1) return
const maxFee = msatsToSats(Math.ceil(excess * (user.autoWithdrawMaxFeePercent / 100.0)))
const amount = msatsToSats(excess) - maxFee
// must be >= 1 sat
if (amount < 1) return
// check that
// 1. the user doesn't have an autowithdraw pending
// 2. we have not already attempted to autowithdraw this fee recently
const [pendingOrFailed] = await models.$queryRaw`
SELECT EXISTS(
SELECT *
FROM "Withdrawl"
WHERE "userId" = ${id} AND "autoWithdraw"
AND (status IS NULL
OR (
status <> 'CONFIRMED' AND
now() < created_at + interval '1 hour' AND
"msatsFeePaying" >= ${satsToMsats(maxFee)}
))
)`
if (pendingOrFailed.exists) return
// get the wallets in order of priority
const wallets = await models.wallet.findMany({
where: { userId: user.id, enabled: true },
orderBy: [
{ priority: 'desc' },
// use id as tie breaker (older wallet first)
{ id: 'asc' }
]
})
for (const wallet of wallets) {
const w = SERVER_WALLET_DEFS.find(({ default: w }) => w.server.walletType === wallet.type)
try {
const { server: { walletType, walletField, createInvoice } } = w.default
return await autowithdraw(
{ walletType, walletField, createInvoice },
{ amount, maxFee },
{ me: user, models, lnd }
)
} catch (error) {
console.error(error)
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
const details = error[2]?.err?.details || error.message || error.toString?.()
await addWalletLog({
wallet,
level: 'ERROR',
message: 'autowithdrawal failed: ' + details
}, { me: user, models })
}
}
// none of the wallets worked
}
async function autowithdraw (
{ walletType, walletField, createInvoice: walletCreateInvoice },
{ amount, maxFee },
{ me, models, lnd }) {
if (!me) {
throw new Error('me not specified')
}
const wallet = await models.wallet.findFirst({
where: {
userId: me.id,
type: walletType
},
include: {
[walletField]: true
}
})
if (!wallet || !wallet[walletField]) {
throw new Error(`no ${walletType} wallet found`)
}
const bolt11 = await walletCreateInvoice(
{ amount, maxFee },
wallet[walletField],
{
me,
models,
lnd,
lnService: {
authenticatedLndGrpc,
createInvoice: lndCreateInvoice,
getIdentity,
decodePaymentRequest
}
})
return await createWithdrawal(null, { invoice: bolt11, maxFee }, { me, models, lnd, walletId: wallet.id })
}
// async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
// if (!me) {
// throw new Error('me not specified')
// }
//
// const wallet = await models.wallet.findFirst({
// where: {
// userId: me.id,
// type: Wallet.CLN.type
// },
// include: {
// walletCLN: true
// }
// })
//
// if (!wallet || !wallet.walletCLN) {
// throw new Error('no cln wallet found')
// }
//
// const { walletCLN: { cert, rune, socket } } = wallet
//
// const inv = await createInvoiceCLN({
// socket,
// rune,
// cert,
// description: me.hideInvoiceDesc ? undefined : 'autowithdraw to CLN from SN',
// msats: amount + 'sat',
// expiry: 360
// })
//
// return await createWithdrawal(null, { invoice: inv.bolt11, maxFee }, { me, models, lnd, walletId: wallet.id })
// }