stacker.news/wallets/server.js
ekzyis a46f81f1e1
Receiver fallbacks (#1688)
* Use same naming scheme between ln containers and env vars

* Add router_lnd container

* Only open channels to router_lnd

* Use 1sat base fee and 0ppm fee rate

* Add script to test routing

* Also fund router_lnd wallet

* Receiver fallbacks

* Rename to predecessorId

* Remove useless wallet table join

* Missing renaming to predecessor

* Fix payment stuck on sender error

We want to await the invoice poll promise so we can check for receiver errors, but in case of sender errors, the promise will never settle.

* Don't log failed forwards as sender errors

* fix check for receiver error

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-12-10 14:15:29 -06:00

206 lines
6.7 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 { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format'
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
import { withTimeout } from '@/lib/time'
import { canReceive } from './common'
import wrapInvoice from './wrap'
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 }, { predecessorId, models }) {
// get the wallets in order of priority
const wallets = await getInvoiceableWallets(userId, { predecessorId, models })
msats = toPositiveNumber(msats)
for (const { def, wallet } of wallets) {
const logger = walletLogger({ wallet, models })
try {
logger.info(
`↙ incoming payment: ${formatSats(msatsToSats(msats))}`,
{
amount: formatMsats(msats)
})
let invoice
try {
invoice = await walletCreateInvoice(
{ wallet, def },
{ msats, description, descriptionHash, expiry },
{ 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, { status: true })
}
}
throw new Error('no wallet to receive available')
}
export async function createWrappedInvoice (userId,
{ msats, feePercent, description, descriptionHash, expiry = 360 },
{ predecessorId, models, me, lnd }) {
let logger, bolt11
try {
const { invoice, wallet } = await createInvoice(userId, {
// this is the amount the stacker will receive, the other (feePercent)% is our fee
msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n,
description,
descriptionHash,
expiry
}, { predecessorId, models })
logger = walletLogger({ wallet, models })
bolt11 = invoice
const { invoice: wrappedInvoice, maxFee } =
await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd })
return {
invoice,
wrappedInvoice: wrappedInvoice.request,
wallet,
maxFee
}
} catch (e) {
logger?.error('invalid invoice: ' + e.message, { bolt11 })
throw e
}
}
export async function getInvoiceableWallets (userId, { predecessorId, models }) {
// filter out all wallets that have already been tried by recursively following the retry chain of predecessor invoices.
// the current predecessor invoice is in state 'FAILED' and not in state 'RETRYING' because we are currently retrying it
// so it has not been updated yet.
// if predecessorId is not provided, the subquery will be empty and thus no wallets are filtered out.
const wallets = await models.$queryRaw`
SELECT
"Wallet".*,
jsonb_build_object(
'id', "users"."id",
'hideInvoiceDesc', "users"."hideInvoiceDesc"
) AS "user"
FROM "Wallet"
JOIN "users" ON "users"."id" = "Wallet"."userId"
WHERE
"Wallet"."userId" = ${userId}
AND "Wallet"."enabled" = true
AND "Wallet"."id" NOT IN (
WITH RECURSIVE "Retries" AS (
-- select the current failed invoice that we are currently retrying
-- this failed invoice will be used to start the recursion
SELECT "Invoice"."id", "Invoice"."predecessorId"
FROM "Invoice"
WHERE "Invoice"."id" = ${predecessorId} AND "Invoice"."actionState" = 'FAILED'
UNION ALL
-- recursive part: use predecessorId to select the previous invoice that failed in the chain
-- until there is no more previous invoice
SELECT "Invoice"."id", "Invoice"."predecessorId"
FROM "Invoice"
JOIN "Retries" ON "Invoice"."id" = "Retries"."predecessorId"
WHERE "Invoice"."actionState" = 'RETRYING'
)
SELECT
"InvoiceForward"."walletId"
FROM "Retries"
JOIN "InvoiceForward" ON "InvoiceForward"."invoiceId" = "Retries"."id"
WHERE "InvoiceForward"."withdrawlId" IS NOT NULL
)
ORDER BY "Wallet"."priority" ASC, "Wallet"."id" ASC`
const walletsWithDefs = wallets.map(wallet => {
const w = walletDefs.find(w => w.walletType === wallet.type)
return { wallet, def: w }
})
return walletsWithDefs.filter(({ def, wallet }) => canReceive({ def, config: wallet.wallet }))
}
async function walletCreateInvoice ({ wallet, def }, {
msats,
description,
descriptionHash,
expiry = 360
}, { logger, models }) {
// 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(
def.createInvoice(
{
msats,
description: wallet.user.hideInvoiceDesc ? undefined : description,
descriptionHash,
expiry
},
wallet.wallet,
{ logger }
), 10_000)
}