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 { datePivot } from '@/lib/time'
|
||||
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 * as ITEM_CREATE from './itemCreate'
|
||||
import * as ITEM_UPDATE from './itemUpdate'
|
||||
|
@ -249,15 +249,19 @@ export async function createLightningInvoice (actionType, args, context) {
|
|||
}
|
||||
|
||||
if (userId) {
|
||||
let logger, bolt11
|
||||
try {
|
||||
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
|
||||
msats: cost * BigInt(7) / BigInt(10),
|
||||
description,
|
||||
expiry: INVOICE_EXPIRE_SECS
|
||||
}, { models })
|
||||
|
||||
logger = walletLogger({ wallet, models })
|
||||
bolt11 = invoice
|
||||
|
||||
// the sender (me) decides if the wrapped invoice has a description
|
||||
// whereas the recipient decides if their invoice has a description
|
||||
const { invoice: wrappedInvoice, maxFee } = await wrapInvoice(
|
||||
|
@ -270,6 +274,7 @@ export async function createLightningInvoice (actionType, args, context) {
|
|||
maxFee
|
||||
}
|
||||
} catch (e) {
|
||||
logger?.error('invalid invoice: ' + e.message, { bolt11 })
|
||||
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 { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { SELECT, itemQueryWithMeta } from './item'
|
||||
import { msatsToSats, msatsToSatsDecimal } from '@/lib/format'
|
||||
import { formatMsats, formatSats, msatsToSats, msatsToSatsDecimal } from '@/lib/format'
|
||||
import {
|
||||
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
|
||||
|
@ -54,20 +54,25 @@ function injectResolvers (resolvers) {
|
|||
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({
|
||||
wallet: {
|
||||
field: walletDef.walletField,
|
||||
type: walletDef.walletType
|
||||
},
|
||||
wallet,
|
||||
testCreateInvoice:
|
||||
walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data })
|
||||
? (data) => walletDef.testCreateInvoice(data, { me, models })
|
||||
? (data) => walletDef.testCreateInvoice(data, { logger, me, models })
|
||||
: null
|
||||
}, {
|
||||
settings,
|
||||
data,
|
||||
vaultEntries
|
||||
}, { me, models })
|
||||
}, { logger, me, models })
|
||||
}
|
||||
}
|
||||
console.groupEnd()
|
||||
|
@ -406,7 +411,7 @@ const resolvers = {
|
|||
userId: me.id,
|
||||
wallet: type ?? undefined,
|
||||
createdAt: {
|
||||
gte: from ? new Date(Number(from)) : undefined,
|
||||
gt: from ? new Date(Number(from)) : undefined,
|
||||
lte: to ? new Date(Number(to)) : undefined
|
||||
}
|
||||
},
|
||||
|
@ -502,7 +507,15 @@ const resolvers = {
|
|||
sendToLnAddr,
|
||||
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
||||
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 } })
|
||||
},
|
||||
dropBolt11: async (parent, { id }, { me, models, lnd }) => {
|
||||
|
@ -561,11 +574,9 @@ const resolvers = {
|
|||
throw new GqlInputError('wallet not found')
|
||||
}
|
||||
|
||||
await models.$transaction([
|
||||
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' } }),
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached for receives' } })
|
||||
])
|
||||
const logger = walletLogger({ wallet, models })
|
||||
await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
|
||||
logger.info('wallet detached')
|
||||
|
||||
return true
|
||||
},
|
||||
|
@ -663,16 +674,47 @@ const resolvers = {
|
|||
|
||||
export default injectResolvers(resolvers)
|
||||
|
||||
export const addWalletLog = async ({ wallet, level, message }, { models }) => {
|
||||
try {
|
||||
await models.walletLog.create({ data: { userId: wallet.userId, wallet: wallet.type, level, message } })
|
||||
} catch (err) {
|
||||
console.error('error creating wallet log:', err)
|
||||
export const walletLogger = ({ wallet, models }) => {
|
||||
// server implementation of wallet logger interface on client
|
||||
const log = (level) => async (message, context = {}) => {
|
||||
try {
|
||||
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 (
|
||||
{ wallet, testCreateInvoice }, { settings, data, vaultEntries }, { me, models }) {
|
||||
{ wallet, testCreateInvoice }, { settings, data, vaultEntries }, { logger, me, models }) {
|
||||
if (!me) {
|
||||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
@ -682,11 +724,8 @@ async function upsertWallet (
|
|||
try {
|
||||
await testCreateInvoice(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const message = 'failed to create test invoice: ' + (err.message || err.toString?.())
|
||||
wallet = { ...wallet, userId: me.id }
|
||||
await addWalletLog({ wallet, level: 'ERROR', message }, { models })
|
||||
await addWalletLog({ wallet, level: 'INFO', message: 'receives disabled' }, { models })
|
||||
logger.error(message)
|
||||
throw new GqlInputError(message)
|
||||
}
|
||||
}
|
||||
|
@ -804,7 +843,7 @@ async function upsertWallet (
|
|||
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 })
|
||||
await validateSchema(withdrawlSchema, { invoice, maxFee })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
@ -836,22 +875,22 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
|
|||
}
|
||||
|
||||
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) {
|
||||
throw new GqlInputError('your invoice amount is too large')
|
||||
throw new GqlInputError('invoice amount is too large')
|
||||
}
|
||||
|
||||
const msatsFee = Number(maxFee) * 1000
|
||||
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
|
||||
const autoWithdraw = !!walletId
|
||||
const autoWithdraw = !!wallet?.id
|
||||
// create withdrawl transactionally (id, bolt11, amount, fee)
|
||||
const [withdrawl] = await serialize(
|
||||
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 }
|
||||
)
|
||||
|
||||
|
|
|
@ -170,6 +170,7 @@ const typeDefs = `
|
|||
wallet: ID!
|
||||
level: String!
|
||||
message: String!
|
||||
context: JSONObject
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -1,15 +1,52 @@
|
|||
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 (
|
||||
<tr className={styles.line}>
|
||||
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
|
||||
{showWallet ? <td className={styles.wallet}>[{wallet}]</td> : <td className='mx-1' />}
|
||||
<td className={`${styles.level} ${levelClassName}`}>{level === 'success' ? 'ok' : level}</td>
|
||||
<td>{message}</td>
|
||||
</tr>
|
||||
<>
|
||||
<tr className={styles.tableRow} onClick={handleClick} style={style}>
|
||||
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
|
||||
{showWallet ? <td className={styles.wallet}>[{wallet}]</td> : <td className='mx-1' />}
|
||||
<td className={`${styles.level} ${className}`}>{level}</td>
|
||||
<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;
|
||||
}
|
|
@ -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 { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import styles from '@/styles/log.module.css'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useToast } from './toast'
|
||||
|
@ -10,6 +10,9 @@ import { gql, useLazyQuery, useMutation } from '@apollo/client'
|
|||
import { useMe } from './me'
|
||||
import useIndexedDB, { getDbName } from './use-indexeddb'
|
||||
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 }) {
|
||||
const { logs, setLogs, hasMore, loadMore, loading } = useWalletLogs(wallet)
|
||||
|
@ -27,8 +30,15 @@ export function WalletLogs ({ wallet, embedded }) {
|
|||
>clear logs
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
|
||||
<div className={`${styles.tableContainer} ${embedded ? styles.embedded : ''}`}>
|
||||
<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>
|
||||
{logs.map((log, i) => (
|
||||
<LogMessage
|
||||
|
@ -103,8 +113,8 @@ function useWalletLogDB () {
|
|||
export function useWalletLogger (wallet, setLogs) {
|
||||
const { add, clear, notSupported } = useWalletLogDB()
|
||||
|
||||
const appendLog = useCallback(async (wallet, level, message) => {
|
||||
const log = { wallet: tag(wallet), level, message, ts: +new Date() }
|
||||
const appendLog = useCallback(async (wallet, level, message, context) => {
|
||||
const log = { wallet: tag(wallet), level, message, ts: +new Date(), context }
|
||||
try {
|
||||
if (notSupported) {
|
||||
console.log('cannot persist wallet log: indexeddb not supported')
|
||||
|
@ -149,20 +159,33 @@ export function useWalletLogger (wallet, setLogs) {
|
|||
}
|
||||
}, [clear, deleteServerWalletLogs, setLogs, notSupported])
|
||||
|
||||
const log = useCallback(level => message => {
|
||||
const log = useCallback(level => (message, context = {}) => {
|
||||
if (!wallet) {
|
||||
// console.error('cannot log: no wallet set')
|
||||
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)
|
||||
}, [appendLog, wallet])
|
||||
|
||||
const logger = useMemo(() => ({
|
||||
ok: (...message) => log('ok')(message.join(' ')),
|
||||
info: (...message) => log('info')(message.join(' ')),
|
||||
error: (...message) => log('error')(message.join(' '))
|
||||
ok: (message, context) => log('ok')(message, context),
|
||||
info: (message, context) => log('info')(message, context),
|
||||
error: (message, context) => log('error')(message, context)
|
||||
}), [log])
|
||||
|
||||
return { logger, deleteLogs }
|
||||
|
@ -176,7 +199,6 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||
const [logs, _setLogs] = useState([])
|
||||
const [page, setPage] = useState(initialPage)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [cursor, setCursor] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
|
@ -191,7 +213,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||
if (newLogs.length === 0) setHasMore(false)
|
||||
}, [logs, _setLogs, setHasMore])
|
||||
|
||||
const loadLogsPage = useCallback(async (page, pageSize, walletDef) => {
|
||||
const loadLogsPage = useCallback(async (page, pageSize, walletDef, variables = {}) => {
|
||||
try {
|
||||
let result = { data: [], hasMore: false }
|
||||
if (notSupported) {
|
||||
|
@ -206,14 +228,38 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||
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({
|
||||
variables: {
|
||||
type: walletDef?.walletType,
|
||||
// if it client logs has more, page based on it's range
|
||||
from: result?.data[result.data.length - 1]?.ts && result.hasMore ? String(result.data[result.data.length - 1].ts) : null,
|
||||
// if we have a cursor (this isn't the first page), page based on it's range
|
||||
to: result?.data[0]?.ts && cursor ? String(result.data[0].ts) : null,
|
||||
cursor
|
||||
from,
|
||||
to,
|
||||
cursor,
|
||||
...variables
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -222,13 +268,17 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||
wallet: tag(getWalletByType(walletType)),
|
||||
...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)
|
||||
return { ...result, data: combinedLogs, hasMore: result.hasMore || !!data.walletLogs.cursor }
|
||||
return {
|
||||
...result,
|
||||
data: combinedLogs,
|
||||
hasMore: result.hasMore || !!data.walletLogs.cursor
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading logs from IndexedDB:', error)
|
||||
return { data: [], total: 0, hasMore: false }
|
||||
return { data: [], hasMore: false }
|
||||
}
|
||||
}, [getPage, setCursor, cursor, notSupported])
|
||||
|
||||
|
@ -240,27 +290,33 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||
if (hasMore) {
|
||||
setLoading(true)
|
||||
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
|
||||
setLogs(prevLogs => [...prevLogs, ...result.data])
|
||||
_setLogs(prevLogs => [...prevLogs, ...result.data])
|
||||
setHasMore(result.hasMore)
|
||||
setTotal(result.total)
|
||||
setPage(prevPage => prevPage + 1)
|
||||
setLoading(false)
|
||||
}
|
||||
}, [loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
|
||||
|
||||
const loadLogs = useCallback(async () => {
|
||||
setLoading(true)
|
||||
const result = await loadLogsPage(1, logsPerPage, wallet?.def)
|
||||
setLogs(result.data)
|
||||
setHasMore(result.hasMore)
|
||||
setTotal(result.total)
|
||||
setPage(1)
|
||||
const loadNew = useCallback(async () => {
|
||||
const newestTs = logs[0]?.ts
|
||||
const variables = { from: newestTs?.toString(), to: null }
|
||||
const result = await loadLogsPage(1, logsPerPage, wallet?.def, variables)
|
||||
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(() => {
|
||||
loadLogs()
|
||||
}, [wallet?.def])
|
||||
useInterval(() => {
|
||||
loadNew().catch(console.error)
|
||||
}, 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
|
||||
level
|
||||
message
|
||||
context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import fetch from 'cross-fetch'
|
||||
import crypto from 'crypto'
|
||||
import { getAgent } from '@/lib/proxy'
|
||||
import { assertContentTypeJson } from './url'
|
||||
|
||||
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
|
||||
const agent = getAgent({ hostname: socket, cert })
|
||||
|
@ -25,6 +26,11 @@ export const createInvoice = async ({ socket, rune, cert, label, description, ms
|
|||
expiry
|
||||
})
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
assertContentTypeJson(res)
|
||||
}
|
||||
|
||||
const inv = await res.json()
|
||||
if (inv.error) {
|
||||
throw new Error(inv.error.message)
|
||||
|
|
|
@ -166,9 +166,9 @@ export function cachedFetcher (fetcher, {
|
|||
logger.incrementCacheMisses()
|
||||
logger.log(`Cache miss for key: ${key}`)
|
||||
const entry = { createdAt: now, pendingPromise: fetchAndCache() }
|
||||
cache.set(key, entry)
|
||||
try {
|
||||
entry.data = await entry.pendingPromise
|
||||
cache.set(key, entry)
|
||||
return entry.data
|
||||
} catch (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)
|
||||
}
|
||||
|
||||
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 => {
|
||||
return btoa(hexstring.match(/\w{2}/g).map(function (a) {
|
||||
return String.fromCharCode(parseInt(a, 16))
|
||||
|
|
|
@ -213,6 +213,13 @@ export function parseNwcUrl (walletConnectUrl) {
|
|||
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) {
|
||||
const parts = imgproxyUrl.split('/')
|
||||
// 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
|
||||
level LogLevel
|
||||
message String
|
||||
context Json? @db.JsonB
|
||||
|
||||
@@index([userId, createdAt])
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
.logNav {
|
||||
text-align: center;
|
||||
color: var(--theme-grey) !important; /* .text-muted */
|
||||
}
|
||||
|
||||
.logTable {
|
||||
.tableContainer {
|
||||
width: 100%;
|
||||
max-height: 60svh;
|
||||
overflow-y: auto;
|
||||
|
@ -14,7 +9,7 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.logTable {
|
||||
.tableContainer {
|
||||
max-height: 70svh;
|
||||
}
|
||||
|
||||
|
@ -22,3 +17,26 @@
|
|||
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 errors = res.errors
|
||||
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
|
||||
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 useVault from '@/components/vault/use-vault'
|
||||
import { useWalletLogger } from '@/components/wallet-logger'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { decode as bolt11Decode } from 'bolt11'
|
||||
import walletDefs from 'wallets/client'
|
||||
import { generateMutation } from './graphql'
|
||||
import { formatSats } from '@/lib/format'
|
||||
|
||||
const WalletsContext = createContext({
|
||||
wallets: []
|
||||
|
@ -231,14 +232,14 @@ export function useWallet (name) {
|
|||
const { logger } = useWalletLogger(wallet?.def)
|
||||
|
||||
const sendPayment = useCallback(async (bolt11) => {
|
||||
const hash = bolt11Tags(bolt11).payment_hash
|
||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
||||
const decoded = bolt11Decode(bolt11)
|
||||
logger.info(`↗ sending payment: ${formatSats(decoded.satoshis)}`, { bolt11 })
|
||||
try {
|
||||
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) {
|
||||
const message = err.message || err.toString?.()
|
||||
logger.error('payment failed:', `payment_hash=${hash}`, message)
|
||||
logger.error(`payment failed: ${message}`, { bolt11 })
|
||||
throw err
|
||||
}
|
||||
}, [wallet, logger])
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { assertContentTypeJson } from '@/lib/url'
|
||||
|
||||
export * from 'wallets/lnbits'
|
||||
|
||||
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 })
|
||||
if (!res.ok) {
|
||||
assertContentTypeJson(res)
|
||||
const errBody = await res.json()
|
||||
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 })
|
||||
if (!res.ok) {
|
||||
assertContentTypeJson(res)
|
||||
const errBody = await res.json()
|
||||
throw new Error(errBody.detail)
|
||||
}
|
||||
|
@ -70,6 +74,7 @@ async function getPayment (paymentHash, { url, adminKey }) {
|
|||
|
||||
const res = await fetch(url + path, { method: 'GET', headers })
|
||||
if (!res.ok) {
|
||||
assertContentTypeJson(res)
|
||||
const errBody = await res.json()
|
||||
throw new Error(errBody.detail)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { msatsToSats } from '@/lib/format'
|
||||
import { getAgent } from '@/lib/proxy'
|
||||
import { assertContentTypeJson } from '@/lib/url'
|
||||
import fetch from 'cross-fetch'
|
||||
|
||||
export * from 'wallets/lnbits'
|
||||
|
@ -44,6 +45,7 @@ export async function createInvoice (
|
|||
body
|
||||
})
|
||||
if (!res.ok) {
|
||||
assertContentTypeJson(res)
|
||||
const errBody = await res.json()
|
||||
throw new Error(errBody.detail)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ async function disconnect (lnc, logger) {
|
|||
}, 50)
|
||||
logger.info('disconnected')
|
||||
} 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('waiting for response ...')
|
||||
logger?.info(`waiting for ${method} response ...`)
|
||||
|
||||
const [response] = await subscription
|
||||
|
||||
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 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.result) return content.result
|
||||
|
||||
throw new Error('invalid response: missing error or result')
|
||||
throw new Error(`invalid ${method} response: missing error or result`)
|
||||
} finally {
|
||||
relay?.close()
|
||||
logger?.info(`closed connection to ${relayUrl}`)
|
||||
|
|
|
@ -2,10 +2,10 @@ import { withTimeout } from '@/lib/time'
|
|||
import { nwcCall, supportedMethods } from 'wallets/nwc'
|
||||
export * from 'wallets/nwc'
|
||||
|
||||
export async function testCreateInvoice ({ nwcUrlRecv }) {
|
||||
export async function testCreateInvoice ({ nwcUrlRecv }, { logger }) {
|
||||
const timeout = 15_000
|
||||
|
||||
const supported = await supportedMethods(nwcUrlRecv, { timeout })
|
||||
const supported = await supportedMethods(nwcUrlRecv, { logger, timeout })
|
||||
|
||||
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 (
|
||||
{ msats, description, expiry },
|
||||
{ nwcUrlRecv }) {
|
||||
{ nwcUrlRecv }, { logger }) {
|
||||
const result = await nwcCall({
|
||||
nwcUrl: nwcUrlRecv,
|
||||
method: 'make_invoice',
|
||||
|
@ -34,6 +34,6 @@ export async function createInvoice (
|
|||
description,
|
||||
expiry
|
||||
}
|
||||
})
|
||||
}, { logger })
|
||||
return result.invoice
|
||||
}
|
||||
|
|
|
@ -25,8 +25,7 @@ export async function sendPayment (bolt11, { url, primaryPassword }) {
|
|||
body
|
||||
})
|
||||
if (!res.ok) {
|
||||
const error = await res.text()
|
||||
throw new Error(error)
|
||||
throw new Error(`POST ${res.url}: ${res.status} ${res.statusText}`)
|
||||
}
|
||||
|
||||
const payment = await res.json()
|
||||
|
|
|
@ -29,8 +29,7 @@ export async function createInvoice (
|
|||
body
|
||||
})
|
||||
if (!res.ok) {
|
||||
const error = await res.text()
|
||||
throw new Error(error)
|
||||
throw new Error(`POST ${res.url}: ${res.status} ${res.statusText}`)
|
||||
}
|
||||
|
||||
const payment = await res.json()
|
||||
|
|
|
@ -11,13 +11,14 @@ import * as blink from 'wallets/blink/server'
|
|||
import * as lnc from 'wallets/lnc'
|
||||
import * as webln from 'wallets/webln'
|
||||
|
||||
import { addWalletLog } from '@/api/resolvers/wallet'
|
||||
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]
|
||||
|
||||
|
@ -41,90 +42,125 @@ export async function createInvoice (userId, { msats, description, descriptionHa
|
|||
|
||||
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 {
|
||||
if (!canReceive({ def: w, config: wallet.wallet })) {
|
||||
continue
|
||||
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 { 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 })
|
||||
|
||||
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 is for an amount greater than requested ${bolt11.mtokens} > ${msats}`)
|
||||
throw new Error('invoice invalid: amount too big')
|
||||
}
|
||||
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) {
|
||||
throw new Error(`invoice has a different satoshi amount ${bolt11.mtokens} !== ${msats}`)
|
||||
throw new Error('invoice invalid: amount too small')
|
||||
}
|
||||
|
||||
await addWalletLog({
|
||||
wallet,
|
||||
level: 'INFO',
|
||||
message: `wallet does not support msats so we floored ${msats} msats to nearest sat ${BigInt(bolt11.mtokens)} msats`
|
||||
}, { models })
|
||||
logger.warn('wallet does not support msats')
|
||||
}
|
||||
|
||||
return { invoice, wallet }
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
await addWalletLog({
|
||||
wallet,
|
||||
level: 'ERROR',
|
||||
message: `creating invoice for ${description ?? ''} failed: ` + error
|
||||
}, { models })
|
||||
return { invoice, wallet, logger }
|
||||
} catch (err) {
|
||||
logger.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const { invoice, wallet } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models })
|
||||
return await createWithdrawal(null,
|
||||
{ invoice, maxFee: msatsToSats(maxFeeMsats) },
|
||||
{ me: { id }, models, lnd, walletId: wallet.id })
|
||||
const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models })
|
||||
|
||||
try {
|
||||
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 { paidActions } from '@/api/paidAction'
|
||||
import { walletLogger } from '@/api/resolvers/wallet'
|
||||
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 { toPositiveNumber } from '@/lib/validate'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
@ -268,7 +270,7 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
|
|||
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 })
|
||||
if (!isConfirmed) {
|
||||
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
|
||||
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 {
|
||||
preimage: payment.secret,
|
||||
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
|
||||
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 {
|
||||
invoiceForward: {
|
||||
update: {
|
||||
withdrawl: {
|
||||
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 { datePivot, sleep } from '@/lib/time'
|
||||
import retry from 'async-retry'
|
||||
import { addWalletLog } from '@/api/resolvers/wallet'
|
||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||
import {
|
||||
paidActionPaid, paidActionForwarded,
|
||||
paidActionFailedForward, paidActionHeld, paidActionFailed,
|
||||
paidActionForwarding,
|
||||
paidActionCanceling
|
||||
} 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) {
|
||||
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 (dbWdrwl.invoiceForward.length > 0) {
|
||||
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 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.withdrawl.update({
|
||||
where: { id: dbWdrwl.id },
|
||||
|
@ -305,35 +307,34 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo
|
|||
], { models })
|
||||
if (code === 0) {
|
||||
notifyWithdrawal(dbWdrwl.userId, wdrwl)
|
||||
if (dbWdrwl.wallet) {
|
||||
// this was an autowithdrawal
|
||||
const message = `autowithdrawal of ${
|
||||
numWithUnits(msatsToSats(paid), { abbreviate: false })} with ${
|
||||
numWithUnits(msatsToSats(fee), { abbreviate: false })} as fee`
|
||||
await addWalletLog({ wallet: dbWdrwl.wallet, level: 'SUCCESS', message }, { models })
|
||||
}
|
||||
|
||||
const { request: bolt11, secret: preimage } = wdrwl.payment
|
||||
logger?.ok(
|
||||
`↙ payment received: ${formatSats(msatsToSats(Number(wdrwl.payment.mtokens)))}`,
|
||||
{
|
||||
bolt11,
|
||||
preimage,
|
||||
fee: formatMsats(fee)
|
||||
})
|
||||
}
|
||||
} else if (wdrwl?.is_failed || notSent) {
|
||||
if (dbWdrwl.invoiceForward.length > 0) {
|
||||
return await paidActionFailedForward({ data: { invoiceId: dbWdrwl.invoiceForward[0].invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
||||
}
|
||||
|
||||
const { status, message } = getPaymentFailureStatus(wdrwl)
|
||||
|
||||
const [{ reverse_withdrawl: code }] = await serialize(
|
||||
const { message, status } = getPaymentFailureStatus(wdrwl)
|
||||
await serialize(
|
||||
models.$queryRaw`
|
||||
SELECT reverse_withdrawl(${dbWdrwl.id}::INTEGER, ${status}::"WithdrawlStatus")`,
|
||||
{ models }
|
||||
)
|
||||
|
||||
if (code === 0 && dbWdrwl.wallet) {
|
||||
// add error into log for autowithdrawal
|
||||
await addWalletLog({
|
||||
wallet: dbWdrwl.wallet,
|
||||
level: 'ERROR',
|
||||
message: 'autowithdrawal failed: ' + message
|
||||
}, { models })
|
||||
}
|
||||
logger?.error(
|
||||
`incoming payment failed: ${message}`,
|
||||
{
|
||||
bolt11: wdrwl.payment.request,
|
||||
max_fee: formatMsats(dbWdrwl.msatsFeePaying)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,7 +385,8 @@ export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss,
|
|||
include: {
|
||||
invoiceForward: {
|
||||
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
|
||||
await checkInvoice({ data: { hash }, models, lnd, ...args })
|
||||
await checkInvoice({ data: { hash }, models, lnd, boss })
|
||||
|
||||
return dbInv
|
||||
}
|
||||
|
||||
export async function checkPendingDeposits (args) {
|
||||
|
|
Loading…
Reference in New Issue