stacker.news/wallets/server.js
ekzyis 72e2d19433
supercharged wallet logs (#1516)
* Inject wallet logger interface

* Include method in NWC logs

* Fix wrong page total

* Poll for new logs every second

* Fix overlapping pagination

* Remove unused total

* Better logs for incoming payments

* Use _setLogs instead of wrapper

* Remove inconsistent receive log

* Remove console.log from wallet logger on server

* Fix missing 'wallet detached' log

* Fix confirm_withdrawl code

* Remove duplicate autowithdrawal log

* Add context to log

* Add more context

* Better table styling

* Move CSS for wallet logs into one file

* remove unused logNav class
* rename classes

* Align key with second column

* Fix TypeError if context empty

* Check content-type header before calling res.json()

* Fix duplicate 'failed to create invoice'

* Parse details from LND error

* Fix invalid DOM property 'colspan'

* P2P zap logs with context

* Remove unnecessary withdrawal error log

* the code assignment was broken anyway
* we already log withdrawal errors using .catch on payViaPaymentRequest

* Don't show outgoing fee to receiver to avoid confusion

* Fix typo in comment

* Log if invoice was canceled by payer

* Automatically populate context from bolt11

* Fix missing context

* Fix wrap errors not logged

* Only log cancel if client canceled

* Remove unused imports

* Log withdrawal/forward success/error in payment flow

* Fix boss not passed to checkInvoice

* Fix TypeError

* Fix database timeouts caused by logger

The logger shares the same connection pool with any currently running transaction.

This means that we enter a classic deadlock when we await logger calls: the logger call is waiting for a connection but the currently running transaction is waiting for the logger call to finish before it can release a connection.

* Fix cache returning undefined

* Fix typo in comment

* Add padding-right to key in log context

* Always use 'incoming payment failed:'

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-11-08 13:26:40 -06:00

167 lines
4.3 KiB
JavaScript

// import server side wallets
import * as lnd from 'wallets/lnd/server'
import * as cln from 'wallets/cln/server'
import * as lnAddr from 'wallets/lightning-address/server'
import * as lnbits from 'wallets/lnbits/server'
import * as nwc from 'wallets/nwc/server'
import * as phoenixd from 'wallets/phoenixd/server'
import * as blink from 'wallets/blink/server'
// we import only the metadata of client side wallets
import * as lnc from 'wallets/lnc'
import * as webln from 'wallets/webln'
import { walletLogger } from '@/api/resolvers/wallet'
import walletDefs from 'wallets/server'
import { parsePaymentRequest } from 'ln-service'
import { toPositiveNumber } from '@/lib/validate'
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
import { withTimeout } from '@/lib/time'
import { canReceive } from './common'
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
const MAX_PENDING_INVOICES_PER_WALLET = 25
export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { models }) {
// get the wallets in order of priority
const wallets = await models.wallet.findMany({
where: { userId, enabled: true },
include: {
user: true
},
orderBy: [
{ priority: 'asc' },
// use id as tie breaker (older wallet first)
{ id: 'asc' }
]
})
msats = toPositiveNumber(msats)
for (const wallet of wallets) {
const w = walletDefs.find(w => w.walletType === wallet.type)
const config = wallet.wallet
if (!canReceive({ def: w, config })) {
continue
}
const logger = walletLogger({ wallet, models })
try {
logger.info(
`↙ incoming payment: ${formatSats(msatsToSats(msats))}`,
{
amount: formatMsats(msats)
})
let invoice
try {
invoice = await walletCreateInvoice(
{ msats, description, descriptionHash, expiry },
{ ...w, userId, createInvoice: w.createInvoice },
{ logger, models })
} catch (err) {
throw new Error('failed to create invoice: ' + err.message)
}
const bolt11 = await parsePaymentRequest({ request: invoice })
logger.info(`created invoice for ${formatSats(msatsToSats(bolt11.mtokens))}`, {
bolt11: invoice
})
if (BigInt(bolt11.mtokens) !== BigInt(msats)) {
if (BigInt(bolt11.mtokens) > BigInt(msats)) {
throw new Error('invoice invalid: amount too big')
}
if (BigInt(bolt11.mtokens) === 0n) {
throw new Error('invoice invalid: amount is 0 msats')
}
if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) {
throw new Error('invoice invalid: amount too small')
}
logger.warn('wallet does not support msats')
}
return { invoice, wallet, logger }
} catch (err) {
logger.error(err.message)
}
}
throw new Error('no wallet to receive available')
}
async function walletCreateInvoice (
{
msats,
description,
descriptionHash,
expiry = 360
},
{
userId,
walletType,
walletField,
createInvoice
},
{ logger, models }) {
const wallet = await models.wallet.findFirst({
where: {
userId,
type: walletType
},
include: {
[walletField]: true,
user: true
}
})
const config = wallet[walletField]
if (!wallet || !config) {
throw new Error('wallet not found')
}
// check for pending withdrawals
const pendingWithdrawals = await models.withdrawl.count({
where: {
walletId: wallet.id,
status: null
}
})
// and pending forwards
const pendingForwards = await models.invoiceForward.count({
where: {
walletId: wallet.id,
invoice: {
actionState: {
notIn: PAID_ACTION_TERMINAL_STATES
}
}
}
})
const pending = pendingWithdrawals + pendingForwards
if (pendingWithdrawals + pendingForwards >= MAX_PENDING_INVOICES_PER_WALLET) {
throw new Error(`too many pending invoices: has ${pending}, max ${MAX_PENDING_INVOICES_PER_WALLET}`)
}
return await withTimeout(
createInvoice(
{
msats,
description: wallet.user.hideInvoiceDesc ? undefined : description,
descriptionHash,
expiry
},
config,
{ logger }
), 10_000)
}