Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wallet Logs #994

Merged
merged 36 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b650ffc
nwc wallet logs
ekzyis Mar 28, 2024
336ac95
persist logs in IndexedDB
ekzyis Mar 29, 2024
e0be8a0
Potential fix for empty error message
ekzyis Mar 29, 2024
1a89dcc
load logs limited to 5m ago from IDB
ekzyis Mar 29, 2024
1663db1
load logs from past via query param
ekzyis Mar 29, 2024
db767a0
Add 5m, 1h, 6h links for earlier logs
ekzyis Mar 29, 2024
b0f4961
Show end of log
ekzyis Mar 30, 2024
f8db214
Clamp to logStart
ekzyis Mar 30, 2024
62c39db
Add log.module.css
ekzyis Mar 30, 2024
e44d32c
Remove TODO about persistence
ekzyis Mar 30, 2024
3f1f489
Use table for logs
ekzyis Mar 31, 2024
69dc7b3
Rename .header to .logNav
ekzyis Mar 31, 2024
68ea329
Simply load all logs and remove navigation
ekzyis Mar 31, 2024
166a793
Add follow checkbox
ekzyis Mar 31, 2024
ebb42e0
Create WalletLogs component
ekzyis Mar 31, 2024
2ef39a4
Embed wallet logs
ekzyis Mar 31, 2024
dae5970
Remove test error
ekzyis Mar 31, 2024
39f8756
Fix level padding
ekzyis Mar 31, 2024
bdd6ac0
Add LNbits logs
ekzyis Mar 31, 2024
3ce22a6
Add logs for attaching LND and lnAddr
ekzyis Apr 3, 2024
c247d2f
Use err.message || err.toString?.() consistently
ekzyis Apr 3, 2024
a6b9985
Autowithdrawal logs
ekzyis Apr 3, 2024
49d2ff0
Use details from LND error
ekzyis Apr 3, 2024
14ba1db
Don't log test invoice individually
ekzyis Apr 3, 2024
823cd9e
Also refetch logs on error
ekzyis Apr 3, 2024
dbbf9b4
Remove obsolete and annoying toasts
ekzyis Apr 3, 2024
b5786e8
Replace scrollIntoView with scroll
ekzyis Apr 3, 2024
326b2a9
Use constant embedded max-height
ekzyis Apr 3, 2024
eefbfe4
Fix missing width: 100% for embedded logs
ekzyis Apr 3, 2024
a20642f
Show full payment hash and preimage in logs
ekzyis Apr 3, 2024
72ca741
Also parse details from LND errors on autowithdrawal failures
ekzyis Apr 3, 2024
ce67132
Remove TODO
ekzyis Apr 3, 2024
ba97b5c
Fix accidental removal of wss:// check
ekzyis Apr 3, 2024
5cf2899
Fix alignment of start marker and show empty if empty
ekzyis Apr 3, 2024
2b632cb
Fix sendPayment loop
ekzyis Apr 3, 2024
2b4e634
Split context in two
ekzyis Apr 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
95 changes: 76 additions & 19 deletions api/resolvers/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ export default {
cursor: history.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
facts: history
}
},
walletLogs: async (parent, args, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
}

return await models.walletLog.findMany({
where: {
userId: me.id
},
orderBy: {
createdAt: 'asc'
}
})
Comment on lines +308 to +320
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are wasting a lot of bandwidth per wallet save here since we always fetch all logs instead of passing the last id the frontend already knows.

But this is as simple as it can be and maybe it's as unnecessary as trying to optimize log performance by not loading all logs from IDB at once but have pagination but that made the code a lot more complicated (see cb1f1a6).

}
},
WalletDetails: {
Expand Down Expand Up @@ -415,35 +429,49 @@ export default {
data.macaroon = ensureB64(data.macaroon)
data.cert = ensureB64(data.cert)

const wallet = 'walletLND'
return await upsertWallet(
{
schema: LNDAutowithdrawSchema,
walletName: 'walletLND',
walletName: wallet,
walletType: 'LND',
testConnect: async ({ cert, macaroon, socket }) => {
const { lnd } = await authenticatedLndGrpc({
cert,
macaroon,
socket
})
return await createInvoice({
description: 'SN connection test',
lnd,
tokens: 0,
expires_at: new Date()
})
try {
const { lnd } = await authenticatedLndGrpc({
cert,
macaroon,
socket
})
const inv = await createInvoice({
description: 'SN connection test',
lnd,
tokens: 0,
expires_at: new Date()
})
// we wrap both calls in one try/catch since connection attempts happen on RPC calls
await addWalletLog({ wallet, level: 'SUCCESS', message: 'connected to LND' }, { me, models })
return inv
} catch (err) {
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
const details = err[2]?.err?.details || err.message || err.toString?.()
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to LND: ${details}` }, { me, models })
throw err
}
}
},
{ settings, data }, { me, models })
},
upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
const wallet = 'walletLightningAddress'
return await upsertWallet(
{
schema: lnAddrAutowithdrawSchema,
walletName: 'walletLightningAddress',
walletName: wallet,
walletType: 'LIGHTNING_ADDRESS',
testConnect: async ({ address }) => {
return await lnAddrOptions(address)
const options = await lnAddrOptions(address)
await addWalletLog({ wallet, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
return options
}
},
{ settings, data }, { me, models })
Expand All @@ -453,7 +481,23 @@ export default {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}

await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
const wallet = await models.wallet.findUnique({ where: { userId: me.id, id: Number(id) } })
if (!wallet) {
throw new GraphQLError('wallet not found', { extensions: { code: 'BAD_INPUT' } })
}

// determine wallet name for logging
let walletName = ''
if (wallet.type === 'LND') {
walletName = 'walletLND'
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
walletName = 'walletLightningAddress'
}

await models.$transaction([
models.wallet.delete({ where: { userId: me.id, id: Number(id) } }),
models.walletLog.create({ data: { userId: me.id, wallet: walletName, level: 'SUCCESS', message: 'wallet deleted' } })
])

return true
}
Expand Down Expand Up @@ -487,6 +531,14 @@ export default {
}
}

export const addWalletLog = async ({ wallet, level, message }, { me, models }) => {
try {
await models.walletLog.create({ data: { userId: me.id, wallet, level, message } })
} catch (err) {
console.error('error creating wallet log:', err)
}
}

async function upsertWallet (
{ schema, walletName, walletType, testConnect }, { settings, data }, { me, models }) {
if (!me) {
Expand All @@ -498,8 +550,9 @@ async function upsertWallet (
if (testConnect) {
try {
await testConnect(data)
} catch (error) {
console.error(error)
} catch (err) {
console.error(err)
await addWalletLog({ wallet: walletName, level: 'ERROR', message: 'failed to attach wallet' }, { me, models })
throw new GraphQLError('failed to connect to wallet', { extensions: { code: 'BAD_INPUT' } })
}
}
Expand Down Expand Up @@ -542,7 +595,9 @@ async function upsertWallet (
}
}
}
}))
}),
models.walletLog.create({ data: { userId: me.id, wallet: walletName, level: 'SUCCESS', message: 'wallet updated' } })
)
} else {
txs.push(
models.wallet.create({
Expand All @@ -554,7 +609,9 @@ async function upsertWallet (
create: walletData
}
}
}))
}),
models.walletLog.create({ data: { userId: me.id, wallet: walletName, level: 'SUCCESS', message: 'wallet created' } })
)
}

await models.$transaction(txs)
Expand Down
9 changes: 9 additions & 0 deletions api/typeDefs/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default gql`
wallets: [Wallet!]!
wallet(id: ID!): Wallet
walletByType(type: String!): Wallet
walletLogs: [WalletLog]!
}

extend type Mutation {
Expand Down Expand Up @@ -99,4 +100,12 @@ export default gql`
facts: [Fact!]!
cursor: String
}

type WalletLog {
id: ID!
createdAt: Date!
wallet: ID!
level: String!
message: String!
}
`
15 changes: 15 additions & 0 deletions components/log-message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { timeSince } from '@/lib/time'
import styles from './log-message.module.css'

export default function LogMessage ({ 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>
<td className={styles.wallet}>[{wallet}]</td>
<td className={`${styles.level} ${levelClassName}`}>{level}</td>
<td>{message}</td>
</tr>
)
}
23 changes: 23 additions & 0 deletions components/log-message.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.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;
}