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>
This commit is contained in:
parent
f1b2197d31
commit
72e2d19433
@ -1,7 +1,7 @@
|
|||||||
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
|
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
|
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
|
||||||
import { createHmac } from '../resolvers/wallet'
|
import { createHmac, walletLogger } from '@/api/resolvers/wallet'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import * as ITEM_CREATE from './itemCreate'
|
import * as ITEM_CREATE from './itemCreate'
|
||||||
import * as ITEM_UPDATE from './itemUpdate'
|
import * as ITEM_UPDATE from './itemUpdate'
|
||||||
@ -249,15 +249,19 @@ export async function createLightningInvoice (actionType, args, context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
let logger, bolt11
|
||||||
try {
|
try {
|
||||||
const description = await paidActions[actionType].describe(args, context)
|
const description = await paidActions[actionType].describe(args, context)
|
||||||
const { invoice: bolt11, wallet } = await createUserInvoice(userId, {
|
const { invoice, wallet } = await createUserInvoice(userId, {
|
||||||
// this is the amount the stacker will receive, the other 3/10ths is the sybil fee
|
// this is the amount the stacker will receive, the other 3/10ths is the sybil fee
|
||||||
msats: cost * BigInt(7) / BigInt(10),
|
msats: cost * BigInt(7) / BigInt(10),
|
||||||
description,
|
description,
|
||||||
expiry: INVOICE_EXPIRE_SECS
|
expiry: INVOICE_EXPIRE_SECS
|
||||||
}, { models })
|
}, { models })
|
||||||
|
|
||||||
|
logger = walletLogger({ wallet, models })
|
||||||
|
bolt11 = invoice
|
||||||
|
|
||||||
// the sender (me) decides if the wrapped invoice has a description
|
// the sender (me) decides if the wrapped invoice has a description
|
||||||
// whereas the recipient decides if their invoice has a description
|
// whereas the recipient decides if their invoice has a description
|
||||||
const { invoice: wrappedInvoice, maxFee } = await wrapInvoice(
|
const { invoice: wrappedInvoice, maxFee } = await wrapInvoice(
|
||||||
@ -270,6 +274,7 @@ export async function createLightningInvoice (actionType, args, context) {
|
|||||||
maxFee
|
maxFee
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger?.error('invalid invoice: ' + e.message, { bolt11 })
|
||||||
console.error('failed to create stacker invoice, falling back to SN invoice', e)
|
console.error('failed to create stacker invoice, falling back to SN invoice', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import crypto, { timingSafeEqual } from 'crypto'
|
|||||||
import serialize from './serial'
|
import serialize from './serial'
|
||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||||
import { SELECT, itemQueryWithMeta } from './item'
|
import { SELECT, itemQueryWithMeta } from './item'
|
||||||
import { msatsToSats, msatsToSatsDecimal } from '@/lib/format'
|
import { formatMsats, formatSats, msatsToSats, msatsToSatsDecimal } from '@/lib/format'
|
||||||
import {
|
import {
|
||||||
ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS,
|
ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS,
|
||||||
INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, LND_PATHFINDING_TIMEOUT_MS
|
INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, LND_PATHFINDING_TIMEOUT_MS
|
||||||
@ -54,20 +54,25 @@ function injectResolvers (resolvers) {
|
|||||||
settings && Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] })
|
settings && Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wallet in shape of db row
|
||||||
|
const wallet = {
|
||||||
|
field: walletDef.walletField,
|
||||||
|
type: walletDef.walletType,
|
||||||
|
userId: me?.id
|
||||||
|
}
|
||||||
|
const logger = walletLogger({ wallet, models })
|
||||||
|
|
||||||
return await upsertWallet({
|
return await upsertWallet({
|
||||||
wallet: {
|
wallet,
|
||||||
field: walletDef.walletField,
|
|
||||||
type: walletDef.walletType
|
|
||||||
},
|
|
||||||
testCreateInvoice:
|
testCreateInvoice:
|
||||||
walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data })
|
walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data })
|
||||||
? (data) => walletDef.testCreateInvoice(data, { me, models })
|
? (data) => walletDef.testCreateInvoice(data, { logger, me, models })
|
||||||
: null
|
: null
|
||||||
}, {
|
}, {
|
||||||
settings,
|
settings,
|
||||||
data,
|
data,
|
||||||
vaultEntries
|
vaultEntries
|
||||||
}, { me, models })
|
}, { logger, me, models })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
@ -406,7 +411,7 @@ const resolvers = {
|
|||||||
userId: me.id,
|
userId: me.id,
|
||||||
wallet: type ?? undefined,
|
wallet: type ?? undefined,
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: from ? new Date(Number(from)) : undefined,
|
gt: from ? new Date(Number(from)) : undefined,
|
||||||
lte: to ? new Date(Number(to)) : undefined
|
lte: to ? new Date(Number(to)) : undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -502,7 +507,15 @@ const resolvers = {
|
|||||||
sendToLnAddr,
|
sendToLnAddr,
|
||||||
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
||||||
verifyHmac(hash, hmac)
|
verifyHmac(hash, hmac)
|
||||||
await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
|
const dbInv = await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
|
||||||
|
|
||||||
|
if (dbInv.invoiceForward) {
|
||||||
|
const { wallet, bolt11 } = dbInv.invoiceForward
|
||||||
|
const logger = walletLogger({ wallet, models })
|
||||||
|
const decoded = await parsePaymentRequest({ request: bolt11 })
|
||||||
|
logger.info(`invoice for ${formatSats(msatsToSats(decoded.mtokens))} canceled by payer`, { bolt11 })
|
||||||
|
}
|
||||||
|
|
||||||
return await models.invoice.findFirst({ where: { hash } })
|
return await models.invoice.findFirst({ where: { hash } })
|
||||||
},
|
},
|
||||||
dropBolt11: async (parent, { id }, { me, models, lnd }) => {
|
dropBolt11: async (parent, { id }, { me, models, lnd }) => {
|
||||||
@ -561,11 +574,9 @@ const resolvers = {
|
|||||||
throw new GqlInputError('wallet not found')
|
throw new GqlInputError('wallet not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
await models.$transaction([
|
const logger = walletLogger({ wallet, models })
|
||||||
models.wallet.delete({ where: { userId: me.id, id: Number(id) } }),
|
await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
|
||||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'INFO', message: 'receives disabled' } }),
|
logger.info('wallet detached')
|
||||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached for receives' } })
|
|
||||||
])
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
@ -663,16 +674,47 @@ const resolvers = {
|
|||||||
|
|
||||||
export default injectResolvers(resolvers)
|
export default injectResolvers(resolvers)
|
||||||
|
|
||||||
export const addWalletLog = async ({ wallet, level, message }, { models }) => {
|
export const walletLogger = ({ wallet, models }) => {
|
||||||
try {
|
// server implementation of wallet logger interface on client
|
||||||
await models.walletLog.create({ data: { userId: wallet.userId, wallet: wallet.type, level, message } })
|
const log = (level) => async (message, context = {}) => {
|
||||||
} catch (err) {
|
try {
|
||||||
console.error('error creating wallet log:', err)
|
if (context?.bolt11) {
|
||||||
|
// automatically populate context from bolt11 to avoid duplicating this code
|
||||||
|
const decoded = await parsePaymentRequest({ request: context.bolt11 })
|
||||||
|
context = {
|
||||||
|
...context,
|
||||||
|
amount: formatMsats(decoded.mtokens),
|
||||||
|
payment_hash: decoded.id,
|
||||||
|
created_at: decoded.created_at,
|
||||||
|
expires_at: decoded.expires_at,
|
||||||
|
description: decoded.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await models.walletLog.create({
|
||||||
|
data: {
|
||||||
|
userId: wallet.userId,
|
||||||
|
wallet: wallet.type,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
context
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('error creating wallet log:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: (message, context) => log('SUCCESS')(message, context),
|
||||||
|
info: (message, context) => log('INFO')(message, context),
|
||||||
|
error: (message, context) => log('ERROR')(message, context),
|
||||||
|
warn: (message, context) => log('WARN')(message, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertWallet (
|
async function upsertWallet (
|
||||||
{ wallet, testCreateInvoice }, { settings, data, vaultEntries }, { me, models }) {
|
{ wallet, testCreateInvoice }, { settings, data, vaultEntries }, { logger, me, models }) {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GqlAuthenticationError()
|
throw new GqlAuthenticationError()
|
||||||
}
|
}
|
||||||
@ -682,11 +724,8 @@ async function upsertWallet (
|
|||||||
try {
|
try {
|
||||||
await testCreateInvoice(data)
|
await testCreateInvoice(data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
|
||||||
const message = 'failed to create test invoice: ' + (err.message || err.toString?.())
|
const message = 'failed to create test invoice: ' + (err.message || err.toString?.())
|
||||||
wallet = { ...wallet, userId: me.id }
|
logger.error(message)
|
||||||
await addWalletLog({ wallet, level: 'ERROR', message }, { models })
|
|
||||||
await addWalletLog({ wallet, level: 'INFO', message: 'receives disabled' }, { models })
|
|
||||||
throw new GqlInputError(message)
|
throw new GqlInputError(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -804,7 +843,7 @@ async function upsertWallet (
|
|||||||
return upsertedWallet
|
return upsertedWallet
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, walletId = null }) {
|
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, wallet, logger }) {
|
||||||
assertApiKeyNotPermitted({ me })
|
assertApiKeyNotPermitted({ me })
|
||||||
await validateSchema(withdrawlSchema, { invoice, maxFee })
|
await validateSchema(withdrawlSchema, { invoice, maxFee })
|
||||||
await assertGofacYourself({ models, headers })
|
await assertGofacYourself({ models, headers })
|
||||||
@ -836,22 +875,22 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!decoded.mtokens || BigInt(decoded.mtokens) <= 0) {
|
if (!decoded.mtokens || BigInt(decoded.mtokens) <= 0) {
|
||||||
throw new GqlInputError('your invoice must specify an amount')
|
throw new GqlInputError('invoice must specify an amount')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decoded.mtokens > Number.MAX_SAFE_INTEGER) {
|
if (decoded.mtokens > Number.MAX_SAFE_INTEGER) {
|
||||||
throw new GqlInputError('your invoice amount is too large')
|
throw new GqlInputError('invoice amount is too large')
|
||||||
}
|
}
|
||||||
|
|
||||||
const msatsFee = Number(maxFee) * 1000
|
const msatsFee = Number(maxFee) * 1000
|
||||||
|
|
||||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
|
||||||
const autoWithdraw = !!walletId
|
const autoWithdraw = !!wallet?.id
|
||||||
// create withdrawl transactionally (id, bolt11, amount, fee)
|
// create withdrawl transactionally (id, bolt11, amount, fee)
|
||||||
const [withdrawl] = await serialize(
|
const [withdrawl] = await serialize(
|
||||||
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
|
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
|
||||||
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name}, ${autoWithdraw}, ${walletId}::INTEGER)`,
|
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name}, ${autoWithdraw}, ${wallet?.id}::INTEGER)`,
|
||||||
{ models }
|
{ models }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -170,6 +170,7 @@ const typeDefs = `
|
|||||||
wallet: ID!
|
wallet: ID!
|
||||||
level: String!
|
level: String!
|
||||||
message: String!
|
message: String!
|
||||||
|
context: JSONObject
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -1,15 +1,52 @@
|
|||||||
import { timeSince } from '@/lib/time'
|
import { timeSince } from '@/lib/time'
|
||||||
import styles from './log-message.module.css'
|
import styles from '@/styles/log.module.css'
|
||||||
|
import { Fragment, useState } from 'react'
|
||||||
|
|
||||||
|
export default function LogMessage ({ showWallet, wallet, level, message, context, ts }) {
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
|
||||||
|
let className
|
||||||
|
switch (level.toLowerCase()) {
|
||||||
|
case 'ok':
|
||||||
|
case 'success':
|
||||||
|
level = 'ok'
|
||||||
|
className = 'text-success'; break
|
||||||
|
case 'error':
|
||||||
|
className = 'text-danger'; break
|
||||||
|
case 'warn':
|
||||||
|
className = 'text-warning'; break
|
||||||
|
default:
|
||||||
|
className = 'text-info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasContext = context && Object.keys(context).length > 0
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (hasContext) { setShow(show => !show) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = hasContext ? { cursor: 'pointer' } : { cursor: 'inherit' }
|
||||||
|
const indicator = hasContext ? (show ? '-' : '+') : <></>
|
||||||
|
|
||||||
export default function LogMessage ({ showWallet, wallet, level, message, ts }) {
|
|
||||||
level = level.toLowerCase()
|
|
||||||
const levelClassName = ['ok', 'success'].includes(level) ? 'text-success' : level === 'error' ? 'text-danger' : level === 'info' ? 'text-info' : ''
|
|
||||||
return (
|
return (
|
||||||
<tr className={styles.line}>
|
<>
|
||||||
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
|
<tr className={styles.tableRow} onClick={handleClick} style={style}>
|
||||||
{showWallet ? <td className={styles.wallet}>[{wallet}]</td> : <td className='mx-1' />}
|
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
|
||||||
<td className={`${styles.level} ${levelClassName}`}>{level === 'success' ? 'ok' : level}</td>
|
{showWallet ? <td className={styles.wallet}>[{wallet}]</td> : <td className='mx-1' />}
|
||||||
<td>{message}</td>
|
<td className={`${styles.level} ${className}`}>{level}</td>
|
||||||
</tr>
|
<td>{message}</td>
|
||||||
|
<td>{indicator}</td>
|
||||||
|
</tr>
|
||||||
|
{show && hasContext && Object.entries(context).map(([key, value], i) => {
|
||||||
|
const last = i === Object.keys(context).length - 1
|
||||||
|
return (
|
||||||
|
<tr className={styles.line} key={i}>
|
||||||
|
<td />
|
||||||
|
<td className={last ? 'pb-2 pe-1' : 'pe-1'} colSpan='2'>{key}</td>
|
||||||
|
<td className={last ? 'text-break pb-2' : 'text-break'}>{value}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
.line {
|
|
||||||
font-family: monospace;
|
|
||||||
color: var(--theme-grey) !important; /* .text-muted */
|
|
||||||
}
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
vertical-align: top;
|
|
||||||
text-align: end;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
justify-self: first baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet {
|
|
||||||
vertical-align: top;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level {
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: top;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
10
components/use-interval.js
Normal file
10
components/use-interval.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
function useInterval (cb, ms, deps) {
|
||||||
|
return useEffect(() => {
|
||||||
|
const interval = setInterval(cb, ms)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, deps)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useInterval
|
@ -1,5 +1,5 @@
|
|||||||
import LogMessage from './log-message'
|
import LogMessage from './log-message'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import styles from '@/styles/log.module.css'
|
import styles from '@/styles/log.module.css'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
@ -10,6 +10,9 @@ import { gql, useLazyQuery, useMutation } from '@apollo/client'
|
|||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import useIndexedDB, { getDbName } from './use-indexeddb'
|
import useIndexedDB, { getDbName } from './use-indexeddb'
|
||||||
import { SSR } from '@/lib/constants'
|
import { SSR } from '@/lib/constants'
|
||||||
|
import useInterval from './use-interval'
|
||||||
|
import { decode as bolt11Decode } from 'bolt11'
|
||||||
|
import { formatMsats } from '@/lib/format'
|
||||||
|
|
||||||
export function WalletLogs ({ wallet, embedded }) {
|
export function WalletLogs ({ wallet, embedded }) {
|
||||||
const { logs, setLogs, hasMore, loadMore, loading } = useWalletLogs(wallet)
|
const { logs, setLogs, hasMore, loadMore, loading } = useWalletLogs(wallet)
|
||||||
@ -27,8 +30,15 @@ export function WalletLogs ({ wallet, embedded }) {
|
|||||||
>clear logs
|
>clear logs
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
|
<div className={`${styles.tableContainer} ${embedded ? styles.embedded : ''}`}>
|
||||||
<table>
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col span='1' style={{ width: '1rem' }} />
|
||||||
|
<col span='1' style={{ width: '1rem' }} />
|
||||||
|
<col span='1' style={{ width: '1rem' }} />
|
||||||
|
<col span='1' style={{ width: '100%' }} />
|
||||||
|
<col span='1' style={{ width: '1rem' }} />
|
||||||
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
{logs.map((log, i) => (
|
{logs.map((log, i) => (
|
||||||
<LogMessage
|
<LogMessage
|
||||||
@ -103,8 +113,8 @@ function useWalletLogDB () {
|
|||||||
export function useWalletLogger (wallet, setLogs) {
|
export function useWalletLogger (wallet, setLogs) {
|
||||||
const { add, clear, notSupported } = useWalletLogDB()
|
const { add, clear, notSupported } = useWalletLogDB()
|
||||||
|
|
||||||
const appendLog = useCallback(async (wallet, level, message) => {
|
const appendLog = useCallback(async (wallet, level, message, context) => {
|
||||||
const log = { wallet: tag(wallet), level, message, ts: +new Date() }
|
const log = { wallet: tag(wallet), level, message, ts: +new Date(), context }
|
||||||
try {
|
try {
|
||||||
if (notSupported) {
|
if (notSupported) {
|
||||||
console.log('cannot persist wallet log: indexeddb not supported')
|
console.log('cannot persist wallet log: indexeddb not supported')
|
||||||
@ -149,20 +159,33 @@ export function useWalletLogger (wallet, setLogs) {
|
|||||||
}
|
}
|
||||||
}, [clear, deleteServerWalletLogs, setLogs, notSupported])
|
}, [clear, deleteServerWalletLogs, setLogs, notSupported])
|
||||||
|
|
||||||
const log = useCallback(level => message => {
|
const log = useCallback(level => (message, context = {}) => {
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
// console.error('cannot log: no wallet set')
|
// console.error('cannot log: no wallet set')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
appendLog(wallet, level, message)
|
if (context?.bolt11) {
|
||||||
|
// automatically populate context from bolt11 to avoid duplicating this code
|
||||||
|
const decoded = bolt11Decode(context.bolt11)
|
||||||
|
context = {
|
||||||
|
...context,
|
||||||
|
amount: formatMsats(Number(decoded.millisatoshis)),
|
||||||
|
payment_hash: decoded.tagsObject.payment_hash,
|
||||||
|
description: decoded.tagsObject.description,
|
||||||
|
created_at: new Date(decoded.timestamp * 1000).toISOString(),
|
||||||
|
expires_at: new Date(decoded.timeExpireDate * 1000).toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLog(wallet, level, message, context)
|
||||||
console[level !== 'error' ? 'info' : 'error'](`[${tag(wallet)}]`, message)
|
console[level !== 'error' ? 'info' : 'error'](`[${tag(wallet)}]`, message)
|
||||||
}, [appendLog, wallet])
|
}, [appendLog, wallet])
|
||||||
|
|
||||||
const logger = useMemo(() => ({
|
const logger = useMemo(() => ({
|
||||||
ok: (...message) => log('ok')(message.join(' ')),
|
ok: (message, context) => log('ok')(message, context),
|
||||||
info: (...message) => log('info')(message.join(' ')),
|
info: (message, context) => log('info')(message, context),
|
||||||
error: (...message) => log('error')(message.join(' '))
|
error: (message, context) => log('error')(message, context)
|
||||||
}), [log])
|
}), [log])
|
||||||
|
|
||||||
return { logger, deleteLogs }
|
return { logger, deleteLogs }
|
||||||
@ -176,7 +199,6 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||||||
const [logs, _setLogs] = useState([])
|
const [logs, _setLogs] = useState([])
|
||||||
const [page, setPage] = useState(initialPage)
|
const [page, setPage] = useState(initialPage)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [total, setTotal] = useState(0)
|
|
||||||
const [cursor, setCursor] = useState(null)
|
const [cursor, setCursor] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
@ -191,7 +213,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||||||
if (newLogs.length === 0) setHasMore(false)
|
if (newLogs.length === 0) setHasMore(false)
|
||||||
}, [logs, _setLogs, setHasMore])
|
}, [logs, _setLogs, setHasMore])
|
||||||
|
|
||||||
const loadLogsPage = useCallback(async (page, pageSize, walletDef) => {
|
const loadLogsPage = useCallback(async (page, pageSize, walletDef, variables = {}) => {
|
||||||
try {
|
try {
|
||||||
let result = { data: [], hasMore: false }
|
let result = { data: [], hasMore: false }
|
||||||
if (notSupported) {
|
if (notSupported) {
|
||||||
@ -206,14 +228,38 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldestTs = result?.data[result.data.length - 1]?.ts // start of local logs
|
||||||
|
const newestTs = result?.data[0]?.ts // end of local logs
|
||||||
|
|
||||||
|
let from
|
||||||
|
if (variables?.from !== undefined) {
|
||||||
|
from = variables.from
|
||||||
|
} else if (oldestTs && result.hasMore) {
|
||||||
|
// fetch all missing, intertwined server logs since start of local logs
|
||||||
|
from = String(oldestTs)
|
||||||
|
} else {
|
||||||
|
from = null
|
||||||
|
}
|
||||||
|
|
||||||
|
let to
|
||||||
|
if (variables?.to !== undefined) {
|
||||||
|
to = variables.to
|
||||||
|
} else if (newestTs && cursor) {
|
||||||
|
// fetch next old page of server logs
|
||||||
|
// ( if cursor is available, we will use decoded time of cursor )
|
||||||
|
to = String(newestTs)
|
||||||
|
} else {
|
||||||
|
to = null
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await getWalletLogs({
|
const { data } = await getWalletLogs({
|
||||||
variables: {
|
variables: {
|
||||||
type: walletDef?.walletType,
|
type: walletDef?.walletType,
|
||||||
// if it client logs has more, page based on it's range
|
from,
|
||||||
from: result?.data[result.data.length - 1]?.ts && result.hasMore ? String(result.data[result.data.length - 1].ts) : null,
|
to,
|
||||||
// if we have a cursor (this isn't the first page), page based on it's range
|
cursor,
|
||||||
to: result?.data[0]?.ts && cursor ? String(result.data[0].ts) : null,
|
...variables
|
||||||
cursor
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -222,13 +268,17 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||||||
wallet: tag(getWalletByType(walletType)),
|
wallet: tag(getWalletByType(walletType)),
|
||||||
...log
|
...log
|
||||||
}))
|
}))
|
||||||
const combinedLogs = Array.from(new Set([...result.data, ...newLogs].map(JSON.stringify))).map(JSON.parse).sort((a, b) => b.ts - a.ts)
|
const combinedLogs = uniqueSort([...result.data, ...newLogs])
|
||||||
|
|
||||||
setCursor(data.walletLogs.cursor)
|
setCursor(data.walletLogs.cursor)
|
||||||
return { ...result, data: combinedLogs, hasMore: result.hasMore || !!data.walletLogs.cursor }
|
return {
|
||||||
|
...result,
|
||||||
|
data: combinedLogs,
|
||||||
|
hasMore: result.hasMore || !!data.walletLogs.cursor
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading logs from IndexedDB:', error)
|
console.error('Error loading logs from IndexedDB:', error)
|
||||||
return { data: [], total: 0, hasMore: false }
|
return { data: [], hasMore: false }
|
||||||
}
|
}
|
||||||
}, [getPage, setCursor, cursor, notSupported])
|
}, [getPage, setCursor, cursor, notSupported])
|
||||||
|
|
||||||
@ -240,27 +290,33 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
|
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
|
||||||
setLogs(prevLogs => [...prevLogs, ...result.data])
|
_setLogs(prevLogs => [...prevLogs, ...result.data])
|
||||||
setHasMore(result.hasMore)
|
setHasMore(result.hasMore)
|
||||||
setTotal(result.total)
|
|
||||||
setPage(prevPage => prevPage + 1)
|
setPage(prevPage => prevPage + 1)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
|
}, [loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
|
||||||
|
|
||||||
const loadLogs = useCallback(async () => {
|
const loadNew = useCallback(async () => {
|
||||||
setLoading(true)
|
const newestTs = logs[0]?.ts
|
||||||
const result = await loadLogsPage(1, logsPerPage, wallet?.def)
|
const variables = { from: newestTs?.toString(), to: null }
|
||||||
setLogs(result.data)
|
const result = await loadLogsPage(1, logsPerPage, wallet?.def, variables)
|
||||||
setHasMore(result.hasMore)
|
|
||||||
setTotal(result.total)
|
|
||||||
setPage(1)
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, [wallet?.def, loadLogsPage])
|
_setLogs(prevLogs => uniqueSort([...result.data, ...prevLogs]))
|
||||||
|
if (!newestTs) {
|
||||||
|
// we only want to update the more button if we didn't fetch new logs since it is about old logs.
|
||||||
|
// we didn't fetch new logs if this is our first fetch (no newest timestamp available)
|
||||||
|
setHasMore(result.hasMore)
|
||||||
|
}
|
||||||
|
}, [logs, wallet?.def, loadLogsPage])
|
||||||
|
|
||||||
useEffect(() => {
|
useInterval(() => {
|
||||||
loadLogs()
|
loadNew().catch(console.error)
|
||||||
}, [wallet?.def])
|
}, 1_000, [loadNew])
|
||||||
|
|
||||||
return { logs, hasMore, total, loadMore, loadLogs, setLogs, loading }
|
return { logs, hasMore: !loading && hasMore, loadMore, setLogs, loading }
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueSort (logs) {
|
||||||
|
return Array.from(new Set(logs.map(JSON.stringify))).map(JSON.parse).sort((a, b) => b.ts - a.ts)
|
||||||
}
|
}
|
||||||
|
@ -191,6 +191,7 @@ export const WALLET_LOGS = gql`
|
|||||||
wallet
|
wallet
|
||||||
level
|
level
|
||||||
message
|
message
|
||||||
|
context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import fetch from 'cross-fetch'
|
import fetch from 'cross-fetch'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { getAgent } from '@/lib/proxy'
|
import { getAgent } from '@/lib/proxy'
|
||||||
|
import { assertContentTypeJson } from './url'
|
||||||
|
|
||||||
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
|
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
|
||||||
const agent = getAgent({ hostname: socket, cert })
|
const agent = getAgent({ hostname: socket, cert })
|
||||||
@ -25,6 +26,11 @@ export const createInvoice = async ({ socket, rune, cert, label, description, ms
|
|||||||
expiry
|
expiry
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
assertContentTypeJson(res)
|
||||||
|
}
|
||||||
|
|
||||||
const inv = await res.json()
|
const inv = await res.json()
|
||||||
if (inv.error) {
|
if (inv.error) {
|
||||||
throw new Error(inv.error.message)
|
throw new Error(inv.error.message)
|
||||||
|
@ -166,9 +166,9 @@ export function cachedFetcher (fetcher, {
|
|||||||
logger.incrementCacheMisses()
|
logger.incrementCacheMisses()
|
||||||
logger.log(`Cache miss for key: ${key}`)
|
logger.log(`Cache miss for key: ${key}`)
|
||||||
const entry = { createdAt: now, pendingPromise: fetchAndCache() }
|
const entry = { createdAt: now, pendingPromise: fetchAndCache() }
|
||||||
cache.set(key, entry)
|
|
||||||
try {
|
try {
|
||||||
entry.data = await entry.pendingPromise
|
entry.data = await entry.pendingPromise
|
||||||
|
cache.set(key, entry)
|
||||||
return entry.data
|
return entry.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.errorLog(`Error fetching data for key: ${key}`, error)
|
logger.errorLog(`Error fetching data for key: ${key}`, error)
|
||||||
|
@ -72,6 +72,9 @@ export const msatsToSatsDecimal = msats => {
|
|||||||
return fixedDecimal(Number(msats) / 1000.0, 3)
|
return fixedDecimal(Number(msats) / 1000.0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatSats = (sats) => numWithUnits(sats, { unitSignular: 'sat', unitPlural: 'sats', abbreviate: false })
|
||||||
|
export const formatMsats = (msats) => numWithUnits(msats, { unitSignular: 'msat', unitPlural: 'msats', abbreviate: false })
|
||||||
|
|
||||||
export const hexToB64 = hexstring => {
|
export const hexToB64 = hexstring => {
|
||||||
return btoa(hexstring.match(/\w{2}/g).map(function (a) {
|
return btoa(hexstring.match(/\w{2}/g).map(function (a) {
|
||||||
return String.fromCharCode(parseInt(a, 16))
|
return String.fromCharCode(parseInt(a, 16))
|
||||||
|
@ -213,6 +213,13 @@ export function parseNwcUrl (walletConnectUrl) {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertContentTypeJson (res) {
|
||||||
|
const contentType = res.headers.get('content-type')
|
||||||
|
if (!contentType || !contentType.includes('application/json')) {
|
||||||
|
throw new Error(`POST ${res.url}: ${res.status} ${res.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function decodeProxyUrl (imgproxyUrl) {
|
export function decodeProxyUrl (imgproxyUrl) {
|
||||||
const parts = imgproxyUrl.split('/')
|
const parts = imgproxyUrl.split('/')
|
||||||
// base64url is not a known encoding in browsers
|
// base64url is not a known encoding in browsers
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "WalletLog" ADD COLUMN "context" JSONB;
|
@ -245,6 +245,7 @@ model WalletLog {
|
|||||||
wallet WalletType
|
wallet WalletType
|
||||||
level LogLevel
|
level LogLevel
|
||||||
message String
|
message String
|
||||||
|
context Json? @db.JsonB
|
||||||
|
|
||||||
@@index([userId, createdAt])
|
@@index([userId, createdAt])
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
.logNav {
|
.tableContainer {
|
||||||
text-align: center;
|
|
||||||
color: var(--theme-grey) !important; /* .text-muted */
|
|
||||||
}
|
|
||||||
|
|
||||||
.logTable {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 60svh;
|
max-height: 60svh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -14,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.logTable {
|
.tableContainer {
|
||||||
max-height: 70svh;
|
max-height: 70svh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,3 +17,26 @@
|
|||||||
max-height: 30svh;
|
max-height: 30svh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tableRow {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--theme-grey) !important; /* .text-muted */
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
vertical-align: top;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
justify-self: first baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet {
|
||||||
|
vertical-align: top;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: top;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
@ -54,7 +54,7 @@ export async function createInvoice (
|
|||||||
const res = out.data.lnInvoiceCreate
|
const res = out.data.lnInvoiceCreate
|
||||||
const errors = res.errors
|
const errors = res.errors
|
||||||
if (errors && errors.length > 0) {
|
if (errors && errors.length > 0) {
|
||||||
throw new Error('failed to create invoice ' + errors.map(e => e.code + ' ' + e.message).join(', '))
|
throw new Error(errors.map(e => e.code + ' ' + e.message).join(', '))
|
||||||
}
|
}
|
||||||
const invoice = res.invoice.paymentRequest
|
const invoice = res.invoice.paymentRequest
|
||||||
return invoice
|
return invoice
|
||||||
|
@ -6,9 +6,10 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState }
|
|||||||
import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally } from './common'
|
import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally } from './common'
|
||||||
import useVault from '@/components/vault/use-vault'
|
import useVault from '@/components/vault/use-vault'
|
||||||
import { useWalletLogger } from '@/components/wallet-logger'
|
import { useWalletLogger } from '@/components/wallet-logger'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { decode as bolt11Decode } from 'bolt11'
|
||||||
import walletDefs from 'wallets/client'
|
import walletDefs from 'wallets/client'
|
||||||
import { generateMutation } from './graphql'
|
import { generateMutation } from './graphql'
|
||||||
|
import { formatSats } from '@/lib/format'
|
||||||
|
|
||||||
const WalletsContext = createContext({
|
const WalletsContext = createContext({
|
||||||
wallets: []
|
wallets: []
|
||||||
@ -231,14 +232,14 @@ export function useWallet (name) {
|
|||||||
const { logger } = useWalletLogger(wallet?.def)
|
const { logger } = useWalletLogger(wallet?.def)
|
||||||
|
|
||||||
const sendPayment = useCallback(async (bolt11) => {
|
const sendPayment = useCallback(async (bolt11) => {
|
||||||
const hash = bolt11Tags(bolt11).payment_hash
|
const decoded = bolt11Decode(bolt11)
|
||||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
logger.info(`↗ sending payment: ${formatSats(decoded.satoshis)}`, { bolt11 })
|
||||||
try {
|
try {
|
||||||
const preimage = await wallet.def.sendPayment(bolt11, wallet.config, { logger })
|
const preimage = await wallet.def.sendPayment(bolt11, wallet.config, { logger })
|
||||||
logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
|
logger.ok(`↗ payment sent: ${formatSats(decoded.satoshis)}`, { bolt11, preimage })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.message || err.toString?.()
|
const message = err.message || err.toString?.()
|
||||||
logger.error('payment failed:', `payment_hash=${hash}`, message)
|
logger.error(`payment failed: ${message}`, { bolt11 })
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [wallet, logger])
|
}, [wallet, logger])
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { assertContentTypeJson } from '@/lib/url'
|
||||||
|
|
||||||
export * from 'wallets/lnbits'
|
export * from 'wallets/lnbits'
|
||||||
|
|
||||||
export async function testSendPayment ({ url, adminKey, invoiceKey }, { logger }) {
|
export async function testSendPayment ({ url, adminKey, invoiceKey }, { logger }) {
|
||||||
@ -32,6 +34,7 @@ async function getWallet ({ url, adminKey, invoiceKey }) {
|
|||||||
|
|
||||||
const res = await fetch(url + path, { method: 'GET', headers })
|
const res = await fetch(url + path, { method: 'GET', headers })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
assertContentTypeJson(res)
|
||||||
const errBody = await res.json()
|
const errBody = await res.json()
|
||||||
throw new Error(errBody.detail)
|
throw new Error(errBody.detail)
|
||||||
}
|
}
|
||||||
@ -52,6 +55,7 @@ async function postPayment (bolt11, { url, adminKey }) {
|
|||||||
|
|
||||||
const res = await fetch(url + path, { method: 'POST', headers, body })
|
const res = await fetch(url + path, { method: 'POST', headers, body })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
assertContentTypeJson(res)
|
||||||
const errBody = await res.json()
|
const errBody = await res.json()
|
||||||
throw new Error(errBody.detail)
|
throw new Error(errBody.detail)
|
||||||
}
|
}
|
||||||
@ -70,6 +74,7 @@ async function getPayment (paymentHash, { url, adminKey }) {
|
|||||||
|
|
||||||
const res = await fetch(url + path, { method: 'GET', headers })
|
const res = await fetch(url + path, { method: 'GET', headers })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
assertContentTypeJson(res)
|
||||||
const errBody = await res.json()
|
const errBody = await res.json()
|
||||||
throw new Error(errBody.detail)
|
throw new Error(errBody.detail)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { msatsToSats } from '@/lib/format'
|
import { msatsToSats } from '@/lib/format'
|
||||||
import { getAgent } from '@/lib/proxy'
|
import { getAgent } from '@/lib/proxy'
|
||||||
|
import { assertContentTypeJson } from '@/lib/url'
|
||||||
import fetch from 'cross-fetch'
|
import fetch from 'cross-fetch'
|
||||||
|
|
||||||
export * from 'wallets/lnbits'
|
export * from 'wallets/lnbits'
|
||||||
@ -44,6 +45,7 @@ export async function createInvoice (
|
|||||||
body
|
body
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
assertContentTypeJson(res)
|
||||||
const errBody = await res.json()
|
const errBody = await res.json()
|
||||||
throw new Error(errBody.detail)
|
throw new Error(errBody.detail)
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ async function disconnect (lnc, logger) {
|
|||||||
}, 50)
|
}, 50)
|
||||||
logger.info('disconnected')
|
logger.info('disconnected')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('failed to disconnect from lnc', err)
|
logger.error('failed to disconnect from lnc: ' + err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,17 +63,17 @@ export async function nwcCall ({ nwcUrl, method, params }, { logger, timeout } =
|
|||||||
|
|
||||||
logger?.info(`published ${method} request`)
|
logger?.info(`published ${method} request`)
|
||||||
|
|
||||||
logger?.info('waiting for response ...')
|
logger?.info(`waiting for ${method} response ...`)
|
||||||
|
|
||||||
const [response] = await subscription
|
const [response] = await subscription
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
throw new Error('no response')
|
throw new Error(`no ${method} response`)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger?.ok('response received')
|
logger?.ok(`${method} response received`)
|
||||||
|
|
||||||
if (!verifyEvent(response)) throw new Error('invalid response: failed to verify')
|
if (!verifyEvent(response)) throw new Error(`invalid ${method} response: failed to verify`)
|
||||||
|
|
||||||
const decrypted = await nip04.decrypt(secret, walletPubkey, response.content)
|
const decrypted = await nip04.decrypt(secret, walletPubkey, response.content)
|
||||||
const content = JSON.parse(decrypted)
|
const content = JSON.parse(decrypted)
|
||||||
@ -81,7 +81,7 @@ export async function nwcCall ({ nwcUrl, method, params }, { logger, timeout } =
|
|||||||
if (content.error) throw new Error(content.error.message)
|
if (content.error) throw new Error(content.error.message)
|
||||||
if (content.result) return content.result
|
if (content.result) return content.result
|
||||||
|
|
||||||
throw new Error('invalid response: missing error or result')
|
throw new Error(`invalid ${method} response: missing error or result`)
|
||||||
} finally {
|
} finally {
|
||||||
relay?.close()
|
relay?.close()
|
||||||
logger?.info(`closed connection to ${relayUrl}`)
|
logger?.info(`closed connection to ${relayUrl}`)
|
||||||
|
@ -2,10 +2,10 @@ import { withTimeout } from '@/lib/time'
|
|||||||
import { nwcCall, supportedMethods } from 'wallets/nwc'
|
import { nwcCall, supportedMethods } from 'wallets/nwc'
|
||||||
export * from 'wallets/nwc'
|
export * from 'wallets/nwc'
|
||||||
|
|
||||||
export async function testCreateInvoice ({ nwcUrlRecv }) {
|
export async function testCreateInvoice ({ nwcUrlRecv }, { logger }) {
|
||||||
const timeout = 15_000
|
const timeout = 15_000
|
||||||
|
|
||||||
const supported = await supportedMethods(nwcUrlRecv, { timeout })
|
const supported = await supportedMethods(nwcUrlRecv, { logger, timeout })
|
||||||
|
|
||||||
const supports = (method) => supported.includes(method)
|
const supports = (method) => supported.includes(method)
|
||||||
|
|
||||||
@ -20,12 +20,12 @@ export async function testCreateInvoice ({ nwcUrlRecv }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { nwcUrlRecv }), timeout)
|
return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { nwcUrlRecv }, { logger }), timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInvoice (
|
export async function createInvoice (
|
||||||
{ msats, description, expiry },
|
{ msats, description, expiry },
|
||||||
{ nwcUrlRecv }) {
|
{ nwcUrlRecv }, { logger }) {
|
||||||
const result = await nwcCall({
|
const result = await nwcCall({
|
||||||
nwcUrl: nwcUrlRecv,
|
nwcUrl: nwcUrlRecv,
|
||||||
method: 'make_invoice',
|
method: 'make_invoice',
|
||||||
@ -34,6 +34,6 @@ export async function createInvoice (
|
|||||||
description,
|
description,
|
||||||
expiry
|
expiry
|
||||||
}
|
}
|
||||||
})
|
}, { logger })
|
||||||
return result.invoice
|
return result.invoice
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,7 @@ export async function sendPayment (bolt11, { url, primaryPassword }) {
|
|||||||
body
|
body
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.text()
|
throw new Error(`POST ${res.url}: ${res.status} ${res.statusText}`)
|
||||||
throw new Error(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const payment = await res.json()
|
const payment = await res.json()
|
||||||
|
@ -29,8 +29,7 @@ export async function createInvoice (
|
|||||||
body
|
body
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.text()
|
throw new Error(`POST ${res.url}: ${res.status} ${res.statusText}`)
|
||||||
throw new Error(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const payment = await res.json()
|
const payment = await res.json()
|
||||||
|
@ -11,13 +11,14 @@ import * as blink from 'wallets/blink/server'
|
|||||||
import * as lnc from 'wallets/lnc'
|
import * as lnc from 'wallets/lnc'
|
||||||
import * as webln from 'wallets/webln'
|
import * as webln from 'wallets/webln'
|
||||||
|
|
||||||
import { addWalletLog } from '@/api/resolvers/wallet'
|
import { walletLogger } from '@/api/resolvers/wallet'
|
||||||
import walletDefs from 'wallets/server'
|
import walletDefs from 'wallets/server'
|
||||||
import { parsePaymentRequest } from 'ln-service'
|
import { parsePaymentRequest } from 'ln-service'
|
||||||
import { toPositiveNumber } from '@/lib/validate'
|
import { toPositiveNumber } from '@/lib/validate'
|
||||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||||
import { withTimeout } from '@/lib/time'
|
import { withTimeout } from '@/lib/time'
|
||||||
import { canReceive } from './common'
|
import { canReceive } from './common'
|
||||||
|
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
||||||
|
|
||||||
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
||||||
|
|
||||||
@ -41,90 +42,125 @@ export async function createInvoice (userId, { msats, description, descriptionHa
|
|||||||
|
|
||||||
for (const wallet of wallets) {
|
for (const wallet of wallets) {
|
||||||
const w = walletDefs.find(w => w.walletType === wallet.type)
|
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 {
|
try {
|
||||||
if (!canReceive({ def: w, config: wallet.wallet })) {
|
logger.info(
|
||||||
continue
|
`↙ 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 { walletType, walletField, createInvoice } = w
|
|
||||||
|
|
||||||
const walletFull = await models.wallet.findFirst({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
type: walletType
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
[walletField]: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!walletFull || !walletFull[walletField]) {
|
|
||||||
throw new Error(`no ${walletType} wallet found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for pending withdrawals
|
|
||||||
const pendingWithdrawals = await models.withdrawl.count({
|
|
||||||
where: {
|
|
||||||
walletId: walletFull.id,
|
|
||||||
status: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// and pending forwards
|
|
||||||
const pendingForwards = await models.invoiceForward.count({
|
|
||||||
where: {
|
|
||||||
walletId: walletFull.id,
|
|
||||||
invoice: {
|
|
||||||
actionState: {
|
|
||||||
notIn: PAID_ACTION_TERMINAL_STATES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('pending invoices', pendingWithdrawals + pendingForwards)
|
|
||||||
if (pendingWithdrawals + pendingForwards >= MAX_PENDING_INVOICES_PER_WALLET) {
|
|
||||||
throw new Error('wallet has too many pending invoices')
|
|
||||||
}
|
|
||||||
console.log('use wallet', walletType)
|
|
||||||
|
|
||||||
const invoice = await withTimeout(
|
|
||||||
createInvoice({
|
|
||||||
msats,
|
|
||||||
description: wallet.user.hideInvoiceDesc ? undefined : description,
|
|
||||||
descriptionHash,
|
|
||||||
expiry
|
|
||||||
}, walletFull[walletField]), 10_000)
|
|
||||||
|
|
||||||
const bolt11 = await parsePaymentRequest({ request: invoice })
|
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)) {
|
||||||
if (BigInt(bolt11.mtokens) > BigInt(msats)) {
|
if (BigInt(bolt11.mtokens) > BigInt(msats)) {
|
||||||
throw new Error(`invoice is for an amount greater than requested ${bolt11.mtokens} > ${msats}`)
|
throw new Error('invoice invalid: amount too big')
|
||||||
}
|
}
|
||||||
if (BigInt(bolt11.mtokens) === 0n) {
|
if (BigInt(bolt11.mtokens) === 0n) {
|
||||||
throw new Error('invoice is for 0 msats')
|
throw new Error('invoice invalid: amount is 0 msats')
|
||||||
}
|
}
|
||||||
if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) {
|
if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) {
|
||||||
throw new Error(`invoice has a different satoshi amount ${bolt11.mtokens} !== ${msats}`)
|
throw new Error('invoice invalid: amount too small')
|
||||||
}
|
}
|
||||||
|
|
||||||
await addWalletLog({
|
logger.warn('wallet does not support msats')
|
||||||
wallet,
|
|
||||||
level: 'INFO',
|
|
||||||
message: `wallet does not support msats so we floored ${msats} msats to nearest sat ${BigInt(bolt11.mtokens)} msats`
|
|
||||||
}, { models })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { invoice, wallet }
|
return { invoice, wallet, logger }
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error(error)
|
logger.error(err.message)
|
||||||
await addWalletLog({
|
|
||||||
wallet,
|
|
||||||
level: 'ERROR',
|
|
||||||
message: `creating invoice for ${description ?? ''} failed: ` + error
|
|
||||||
}, { models })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('no wallet available')
|
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)
|
||||||
}
|
}
|
||||||
|
@ -42,8 +42,14 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|||||||
|
|
||||||
if (pendingOrFailed.exists) return
|
if (pendingOrFailed.exists) return
|
||||||
|
|
||||||
const { invoice, wallet } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models })
|
const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models })
|
||||||
return await createWithdrawal(null,
|
|
||||||
{ invoice, maxFee: msatsToSats(maxFeeMsats) },
|
try {
|
||||||
{ me: { id }, models, lnd, walletId: wallet.id })
|
return await createWithdrawal(null,
|
||||||
|
{ invoice, maxFee: msatsToSats(maxFeeMsats) },
|
||||||
|
{ me: { id }, models, lnd, wallet, logger })
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`incoming payment failed: ${err}`, { bolt11: invoice })
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { getPaymentFailureStatus, hodlInvoiceCltvDetails } from '@/api/lnd'
|
import { getPaymentFailureStatus, hodlInvoiceCltvDetails } from '@/api/lnd'
|
||||||
import { paidActions } from '@/api/paidAction'
|
import { paidActions } from '@/api/paidAction'
|
||||||
|
import { walletLogger } from '@/api/resolvers/wallet'
|
||||||
import { LND_PATHFINDING_TIMEOUT_MS, PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
import { LND_PATHFINDING_TIMEOUT_MS, PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||||
|
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import { toPositiveNumber } from '@/lib/validate'
|
import { toPositiveNumber } from '@/lib/validate'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
@ -268,7 +270,7 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
|
|||||||
throw new Error('invoice is not held')
|
throw new Error('invoice is not held')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hash, msatsPaying } = dbInvoice.invoiceForward.withdrawl
|
const { bolt11, hash, msatsPaying } = dbInvoice.invoiceForward.withdrawl
|
||||||
const { payment, is_confirmed: isConfirmed } = withdrawal ?? await getPayment({ id: hash, lnd })
|
const { payment, is_confirmed: isConfirmed } = withdrawal ?? await getPayment({ id: hash, lnd })
|
||||||
if (!isConfirmed) {
|
if (!isConfirmed) {
|
||||||
throw new Error('payment is not confirmed')
|
throw new Error('payment is not confirmed')
|
||||||
@ -277,6 +279,17 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
|
|||||||
// settle the invoice, allowing us to transition to PAID
|
// settle the invoice, allowing us to transition to PAID
|
||||||
await settleHodlInvoice({ secret: payment.secret, lnd })
|
await settleHodlInvoice({ secret: payment.secret, lnd })
|
||||||
|
|
||||||
|
const logger = walletLogger({ wallet: dbInvoice.invoiceForward.wallet, models })
|
||||||
|
logger.ok(
|
||||||
|
`↙ payment received: ${formatSats(msatsToSats(payment.mtokens))}`,
|
||||||
|
{
|
||||||
|
bolt11,
|
||||||
|
preimage: payment.secret
|
||||||
|
// we could show the outgoing fee that we paid from the incoming amount to the receiver
|
||||||
|
// but we don't since it might look like the receiver paid the fee but that's not the case.
|
||||||
|
// fee: formatMsats(Number(payment.fee_mtokens))
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
preimage: payment.secret,
|
preimage: payment.secret,
|
||||||
invoiceForward: {
|
invoiceForward: {
|
||||||
@ -330,12 +343,21 @@ export async function paidActionFailedForward ({ data: { invoiceId, withdrawal:
|
|||||||
// which once it does succeed will ensure we will try to cancel the held invoice until it actually cancels
|
// which once it does succeed will ensure we will try to cancel the held invoice until it actually cancels
|
||||||
await boss.send('finalizeHodlInvoice', { hash: dbInvoice.hash }, FINALIZE_OPTIONS)
|
await boss.send('finalizeHodlInvoice', { hash: dbInvoice.hash }, FINALIZE_OPTIONS)
|
||||||
|
|
||||||
|
const { status, message } = getPaymentFailureStatus(withdrawal)
|
||||||
|
const { bolt11, msatsFeePaying } = dbInvoice.invoiceForward.withdrawl
|
||||||
|
const logger = walletLogger({ wallet: dbInvoice.invoiceForward.wallet, models })
|
||||||
|
logger.warn(
|
||||||
|
`incoming payment failed: ${message}`, {
|
||||||
|
bolt11,
|
||||||
|
max_fee: formatMsats(Number(msatsFeePaying))
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invoiceForward: {
|
invoiceForward: {
|
||||||
update: {
|
update: {
|
||||||
withdrawl: {
|
withdrawl: {
|
||||||
update: {
|
update: {
|
||||||
status: getPaymentFailureStatus(withdrawal).status
|
status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,15 @@ import { notifyDeposit, notifyWithdrawal } from '@/lib/webPush'
|
|||||||
import { INVOICE_RETENTION_DAYS, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
|
import { INVOICE_RETENTION_DAYS, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
|
||||||
import { datePivot, sleep } from '@/lib/time'
|
import { datePivot, sleep } from '@/lib/time'
|
||||||
import retry from 'async-retry'
|
import retry from 'async-retry'
|
||||||
import { addWalletLog } from '@/api/resolvers/wallet'
|
|
||||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
|
||||||
import {
|
import {
|
||||||
paidActionPaid, paidActionForwarded,
|
paidActionPaid, paidActionForwarded,
|
||||||
paidActionFailedForward, paidActionHeld, paidActionFailed,
|
paidActionFailedForward, paidActionHeld, paidActionFailed,
|
||||||
paidActionForwarding,
|
paidActionForwarding,
|
||||||
paidActionCanceling
|
paidActionCanceling
|
||||||
} from './paidAction'
|
} from './paidAction'
|
||||||
import { getPaymentFailureStatus } from '@/api/lnd/index'
|
import { getPaymentFailureStatus } from '@/api/lnd/index.js'
|
||||||
|
import { walletLogger } from '@/api/resolvers/wallet.js'
|
||||||
|
import { formatMsats, formatSats, msatsToSats } from '@/lib/format.js'
|
||||||
|
|
||||||
export async function subscribeToWallet (args) {
|
export async function subscribeToWallet (args) {
|
||||||
await subscribeToDeposits(args)
|
await subscribeToDeposits(args)
|
||||||
@ -287,6 +287,8 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logger = walletLogger({ models, wallet: dbWdrwl.wallet })
|
||||||
|
|
||||||
if (wdrwl?.is_confirmed) {
|
if (wdrwl?.is_confirmed) {
|
||||||
if (dbWdrwl.invoiceForward.length > 0) {
|
if (dbWdrwl.invoiceForward.length > 0) {
|
||||||
return await paidActionForwarded({ data: { invoiceId: dbWdrwl.invoiceForward[0].invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
return await paidActionForwarded({ data: { invoiceId: dbWdrwl.invoiceForward[0].invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
||||||
@ -294,7 +296,7 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo
|
|||||||
|
|
||||||
const fee = Number(wdrwl.payment.fee_mtokens)
|
const fee = Number(wdrwl.payment.fee_mtokens)
|
||||||
const paid = Number(wdrwl.payment.mtokens) - fee
|
const paid = Number(wdrwl.payment.mtokens) - fee
|
||||||
const [{ confirm_withdrawl: code }] = await serialize([
|
const [[{ confirm_withdrawl: code }]] = await serialize([
|
||||||
models.$queryRaw`SELECT confirm_withdrawl(${dbWdrwl.id}::INTEGER, ${paid}, ${fee})`,
|
models.$queryRaw`SELECT confirm_withdrawl(${dbWdrwl.id}::INTEGER, ${paid}, ${fee})`,
|
||||||
models.withdrawl.update({
|
models.withdrawl.update({
|
||||||
where: { id: dbWdrwl.id },
|
where: { id: dbWdrwl.id },
|
||||||
@ -305,35 +307,34 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo
|
|||||||
], { models })
|
], { models })
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
notifyWithdrawal(dbWdrwl.userId, wdrwl)
|
notifyWithdrawal(dbWdrwl.userId, wdrwl)
|
||||||
if (dbWdrwl.wallet) {
|
|
||||||
// this was an autowithdrawal
|
const { request: bolt11, secret: preimage } = wdrwl.payment
|
||||||
const message = `autowithdrawal of ${
|
logger?.ok(
|
||||||
numWithUnits(msatsToSats(paid), { abbreviate: false })} with ${
|
`↙ payment received: ${formatSats(msatsToSats(Number(wdrwl.payment.mtokens)))}`,
|
||||||
numWithUnits(msatsToSats(fee), { abbreviate: false })} as fee`
|
{
|
||||||
await addWalletLog({ wallet: dbWdrwl.wallet, level: 'SUCCESS', message }, { models })
|
bolt11,
|
||||||
}
|
preimage,
|
||||||
|
fee: formatMsats(fee)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else if (wdrwl?.is_failed || notSent) {
|
} else if (wdrwl?.is_failed || notSent) {
|
||||||
if (dbWdrwl.invoiceForward.length > 0) {
|
if (dbWdrwl.invoiceForward.length > 0) {
|
||||||
return await paidActionFailedForward({ data: { invoiceId: dbWdrwl.invoiceForward[0].invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
return await paidActionFailedForward({ data: { invoiceId: dbWdrwl.invoiceForward[0].invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status, message } = getPaymentFailureStatus(wdrwl)
|
const { message, status } = getPaymentFailureStatus(wdrwl)
|
||||||
|
await serialize(
|
||||||
const [{ reverse_withdrawl: code }] = await serialize(
|
|
||||||
models.$queryRaw`
|
models.$queryRaw`
|
||||||
SELECT reverse_withdrawl(${dbWdrwl.id}::INTEGER, ${status}::"WithdrawlStatus")`,
|
SELECT reverse_withdrawl(${dbWdrwl.id}::INTEGER, ${status}::"WithdrawlStatus")`,
|
||||||
{ models }
|
{ models }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (code === 0 && dbWdrwl.wallet) {
|
logger?.error(
|
||||||
// add error into log for autowithdrawal
|
`incoming payment failed: ${message}`,
|
||||||
await addWalletLog({
|
{
|
||||||
wallet: dbWdrwl.wallet,
|
bolt11: wdrwl.payment.request,
|
||||||
level: 'ERROR',
|
max_fee: formatMsats(dbWdrwl.msatsFeePaying)
|
||||||
message: 'autowithdrawal failed: ' + message
|
})
|
||||||
}, { models })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +385,8 @@ export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss,
|
|||||||
include: {
|
include: {
|
||||||
invoiceForward: {
|
invoiceForward: {
|
||||||
include: {
|
include: {
|
||||||
withdrawl: true
|
withdrawl: true,
|
||||||
|
wallet: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -402,7 +404,9 @@ export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sync LND invoice status with invoice status in database
|
// sync LND invoice status with invoice status in database
|
||||||
await checkInvoice({ data: { hash }, models, lnd, ...args })
|
await checkInvoice({ data: { hash }, models, lnd, boss })
|
||||||
|
|
||||||
|
return dbInv
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkPendingDeposits (args) {
|
export async function checkPendingDeposits (args) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user