Allow deletion of wallet logs (#1101)

* Allow deletion of wallet logs

* Refactor wallet logs client<>server glue code

* Use variant='link' and className='text-muted fw-bold nav-link' for clear & cancel

There is a bug though: 'clear' stays highlighted after modal is closed

* Include wallet in toast

* Delete logs on logout

* Fix ugly wallet name in confirm dialog

* Fix clear still highlighted after modal closed

* Only delete client wallet logs

* Fix ugly wallet name in toast

* Fix bad search and replace

* Use Wallet object as constant

* Also delete LNC logs on logout

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
ekzyis 2024-05-03 14:14:33 -05:00 committed by GitHub
parent e5f8c4e8e8
commit 4961cc045b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 216 additions and 88 deletions

View File

@ -7,7 +7,7 @@ import { SELECT } from './item'
import { lnAddrOptions } from '@/lib/lnurl'
import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format'
import { CLNAutowithdrawSchema, LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '@/lib/validate'
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants'
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, Wallet } from '@/lib/constants'
import { datePivot } from '@/lib/time'
import assertGofacYourself from './ofac'
import assertApiKeyNotPermitted from './apiKey'
@ -434,11 +434,11 @@ export default {
data.macaroon = ensureB64(data.macaroon)
data.cert = ensureB64(data.cert)
const walletType = 'LND'
const wallet = Wallet.LND
return await upsertWallet(
{
schema: LNDAutowithdrawSchema,
walletType,
wallet,
testConnect: async ({ cert, macaroon, socket }) => {
try {
const { lnd } = await authenticatedLndGrpc({
@ -453,12 +453,12 @@ export default {
expires_at: new Date()
})
// we wrap both calls in one try/catch since connection attempts happen on RPC calls
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'connected to LND' }, { me, models })
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: walletType, level: 'ERROR', message: `could not connect to LND: ${details}` }, { me, models })
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to LND: ${details}` }, { me, models })
throw err
}
}
@ -468,11 +468,11 @@ export default {
upsertWalletCLN: async (parent, { settings, ...data }, { me, models }) => {
data.cert = ensureB64(data.cert)
const walletType = 'CLN'
const wallet = Wallet.CLN
return await upsertWallet(
{
schema: CLNAutowithdrawSchema,
walletType,
wallet,
testConnect: async ({ socket, rune, cert }) => {
try {
const inv = await createInvoiceCLN({
@ -483,11 +483,11 @@ export default {
msats: 'any',
expiry: 0
})
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'connected to CLN' }, { me, models })
await addWalletLog({ wallet, level: 'SUCCESS', message: 'connected to CLN' }, { me, models })
return inv
} catch (err) {
const details = err.details || err.message || err.toString?.()
await addWalletLog({ wallet: walletType, level: 'ERROR', message: `could not connect to CLN: ${details}` }, { me, models })
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to CLN: ${details}` }, { me, models })
throw err
}
}
@ -495,14 +495,14 @@ export default {
{ settings, data }, { me, models })
},
upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
const walletType = 'LIGHTNING_ADDRESS'
const wallet = Wallet.LnAddr
return await upsertWallet(
{
schema: lnAddrAutowithdrawSchema,
walletType,
wallet,
testConnect: async ({ address }) => {
const options = await lnAddrOptions(address)
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
await addWalletLog({ wallet, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
return options
}
},
@ -523,6 +523,15 @@ export default {
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet deleted' } })
])
return true
},
deleteWalletLogs: async (parent, { wallet }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
await models.walletLog.deleteMany({ where: { userId: me.id, wallet } })
return true
}
},
@ -557,14 +566,14 @@ export default {
export const addWalletLog = async ({ wallet, level, message }, { me, models }) => {
try {
await models.walletLog.create({ data: { userId: me.id, wallet, level, message } })
await models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level, message } })
} catch (err) {
console.error('error creating wallet log:', err)
}
}
async function upsertWallet (
{ schema, walletType, testConnect }, { settings, data }, { me, models }) {
{ schema, wallet, testConnect }, { settings, data }, { me, models }) {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
@ -577,7 +586,7 @@ async function upsertWallet (
await testConnect(data)
} catch (err) {
console.error(err)
await addWalletLog({ wallet: walletType, level: 'ERROR', message: 'failed to attach wallet' }, { me, models })
await addWalletLog({ wallet, level: 'ERROR', message: 'failed to attach wallet' }, { me, models })
throw new GraphQLError('failed to connect to wallet', { extensions: { code: 'BAD_INPUT' } })
}
}
@ -607,16 +616,13 @@ async function upsertWallet (
}))
}
const walletName = walletType === 'LND'
? 'walletLND'
: walletType === 'CLN' ? 'walletCLN' : 'walletLightningAddress'
if (id) {
txs.push(
models.wallet.update({
where: { id: Number(id), userId: me.id },
data: {
priority: priority ? 1 : 0,
[walletName]: {
[wallet.field]: {
update: {
where: { walletId: Number(id) },
data: walletData
@ -624,7 +630,7 @@ async function upsertWallet (
}
}
}),
models.walletLog.create({ data: { userId: me.id, wallet: walletType, level: 'SUCCESS', message: 'wallet updated' } })
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet updated' } })
)
} else {
txs.push(
@ -632,13 +638,13 @@ async function upsertWallet (
data: {
priority: Number(priority),
userId: me.id,
type: walletType,
[walletName]: {
type: wallet.type,
[wallet.field]: {
create: walletData
}
}
}),
models.walletLog.create({ data: { userId: me.id, wallet: walletType, level: 'SUCCESS', message: 'wallet created' } })
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet created' } })
)
}
@ -751,7 +757,7 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...
if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') {
// unset lnaddr so we don't trigger another withdrawal with same destination
await models.wallet.deleteMany({
where: { userId: me.id, type: 'LIGHTNING_ADDRESS' }
where: { userId: me.id, type: Wallet.LnAddr.type }
})
throw new Error('automated withdrawals to other stackers are not allowed')
}

View File

@ -23,6 +23,7 @@ export default gql`
upsertWalletCLN(id: ID, socket: String!, rune: String!, cert: String, settings: AutowithdrawSettings!): Boolean
upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean
removeWallet(id: ID!): Boolean
deleteWalletLogs(wallet: String): Boolean
}
type Wallet {

View File

@ -1,8 +1,9 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useMe } from './me'
import fancyNames from '@/lib/fancy-names.json'
import { useQuery } from '@apollo/client'
import { gql, useMutation, useQuery } from '@apollo/client'
import { WALLET_LOGS } from '@/fragments/wallet'
import { getWalletBy } from '@/lib/constants'
const generateFancyName = () => {
// 100 adjectives * 100 nouns * 10000 = 100M possible names
@ -157,21 +158,6 @@ const initIndexedDB = async (storeName) => {
})
}
const renameWallet = (wallet) => {
switch (wallet) {
case 'walletLightningAddress':
case 'LIGHTNING_ADDRESS':
return 'lnAddr'
case 'walletLND':
case 'LND':
return 'lnd'
case 'walletCLN':
case 'CLN':
return 'cln'
}
return wallet
}
const WalletLoggerProvider = ({ children }) => {
const [logs, setLogs] = useState([])
const idbStoreName = 'wallet_logs'
@ -187,12 +173,33 @@ const WalletLoggerProvider = ({ children }) => {
const existingIds = prevLogs.map(({ id }) => id)
const logs = walletLogs
.filter(({ id }) => !existingIds.includes(id))
.map(({ createdAt, wallet, ...log }) => ({ ts: +new Date(createdAt), wallet: renameWallet(wallet), ...log }))
.map(({ createdAt, wallet: walletType, ...log }) => {
return {
ts: +new Date(createdAt),
wallet: getWalletBy('type', walletType).logTag,
...log
}
})
return [...prevLogs, ...logs].sort((a, b) => a.ts - b.ts)
})
}
})
const [deleteServerWalletLogs] = useMutation(
gql`
mutation deleteWalletLogs($wallet: String) {
deleteWalletLogs(wallet: $wallet)
}
`,
{
onCompleted: (_, { variables: { wallet: walletType } }) => {
setLogs((logs) => {
return logs.filter(l => walletType ? l.wallet !== getWalletBy('type', walletType).logTag : false)
})
}
}
)
const saveLog = useCallback((log) => {
if (!idb.current) {
// IDB may not be ready yet
@ -234,14 +241,36 @@ const WalletLoggerProvider = ({ children }) => {
}, [])
const appendLog = useCallback((wallet, level, message) => {
const log = { wallet, level, message, ts: +new Date() }
const log = { wallet: wallet.logTag, level, message, ts: +new Date() }
saveLog(log)
setLogs((prevLogs) => [...prevLogs, log])
}, [saveLog])
const deleteLogs = useCallback(async (wallet) => {
if (!wallet || wallet.server) {
await deleteServerWalletLogs({ variables: { wallet: wallet?.type } })
}
if (!wallet || !wallet.server) {
const tx = idb.current.transaction(idbStoreName, 'readwrite')
const objectStore = tx.objectStore(idbStoreName)
const idx = objectStore.index('wallet_ts')
const request = wallet ? idx.openCursor(window.IDBKeyRange.bound([wallet.logTag, -Infinity], [wallet.logTag, Infinity])) : idx.openCursor()
request.onsuccess = function (event) {
const cursor = event.target.result
if (cursor) {
cursor.delete()
cursor.continue()
} else {
// finished
setLogs((logs) => logs.filter(l => wallet ? l.wallet !== wallet.logTag : false))
}
}
}
}, [setLogs])
return (
<WalletLogsContext.Provider value={logs}>
<WalletLoggerContext.Provider value={appendLog}>
<WalletLoggerContext.Provider value={{ appendLog, deleteLogs }}>
{children}
</WalletLoggerContext.Provider>
</WalletLogsContext.Provider>
@ -249,14 +278,14 @@ const WalletLoggerProvider = ({ children }) => {
}
export function useWalletLogger (wallet) {
const appendLog = useContext(WalletLoggerContext)
const { appendLog, deleteLogs: innerDeleteLogs } = useContext(WalletLoggerContext)
const log = useCallback(level => message => {
// TODO:
// also send this to us if diagnostics was enabled,
// very similar to how the service worker logger works.
appendLog(wallet, level, message)
console[level !== 'error' ? 'info' : 'error'](`[${wallet}]`, message)
console[level !== 'error' ? 'info' : 'error'](`[${wallet.logTag}]`, message)
}, [appendLog, wallet])
const logger = useMemo(() => ({
@ -265,10 +294,12 @@ export function useWalletLogger (wallet) {
error: (...message) => log('error')(message.join(' '))
}), [log, wallet])
return logger
const deleteLogs = useCallback((w) => innerDeleteLogs(w || wallet), [innerDeleteLogs, wallet])
return { logger, deleteLogs }
}
export function useWalletLogs (wallet) {
const logs = useContext(WalletLogsContext)
return logs.filter(l => !wallet || l.wallet === wallet)
return logs.filter(l => !wallet || l.wallet === wallet.logTag)
}

View File

@ -6,7 +6,7 @@ import BackArrow from '../../svgs/arrow-left-line.svg'
import { useCallback, useEffect, useState } from 'react'
import Price from '../price'
import SubSelect from '../sub-select'
import { ANON_USER_ID, BALANCE_LIMIT_MSATS } from '../../lib/constants'
import { ANON_USER_ID, BALANCE_LIMIT_MSATS, Wallet } from '../../lib/constants'
import Head from 'next/head'
import NoteIcon from '../../svgs/notification-4-fill.svg'
import { useMe } from '../me'
@ -22,6 +22,7 @@ import SearchIcon from '../../svgs/search-line.svg'
import classNames from 'classnames'
import SnIcon from '@/svgs/sn.svg'
import { useHasNewNotes } from '../use-has-new-notes'
import { useWalletLogger } from '../logger'
export function Brand ({ className }) {
return (
@ -162,6 +163,7 @@ export function NavWalletSummary ({ className }) {
export function MeDropdown ({ me, dropNavKey }) {
if (!me) return null
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
const { deleteLogs } = useWalletLogger()
return (
<div className='position-relative'>
<Dropdown className={styles.dropdown} align='end'>
@ -202,16 +204,16 @@ export function MeDropdown ({ me, dropNavKey }) {
<Dropdown.Divider />
<Dropdown.Item
onClick={async () => {
try {
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
if (pushSubscription) {
await togglePushSubscription()
}
} catch (err) {
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
if (pushSubscription) {
// don't prevent signout because of an unsubscription error
console.error(err)
await togglePushSubscription().catch(console.error)
}
// delete client wallet logs to prevent leak of private data if a shared device was used
await deleteLogs(Wallet.NWC).catch(console.error)
await deleteLogs(Wallet.LNbits).catch(console.error)
await deleteLogs(Wallet.LNC).catch(console.error)
await signOut({ callbackUrl: '/' })
}}
>logout

View File

@ -1,10 +1,13 @@
import { useRouter } from 'next/router'
import LogMessage from './log-message'
import { useWalletLogs } from './logger'
import { useWalletLogger, useWalletLogs } from './logger'
import { useEffect, useRef, useState } from 'react'
import { Checkbox, Form } from './form'
import { useField } from 'formik'
import styles from '@/styles/log.module.css'
import { Button } from 'react-bootstrap'
import { useToast } from './toast'
import { useShowModal } from './modal'
const FollowCheckbox = ({ value, ...props }) => {
const [,, helpers] = useField(props.name)
@ -26,6 +29,7 @@ export default function WalletLogs ({ wallet, embedded }) {
const [follow, setFollow] = useState(defaultFollow ?? true)
const tableRef = useRef()
const scrollY = useRef()
const showModal = useShowModal()
useEffect(() => {
if (follow) {
@ -57,12 +61,21 @@ export default function WalletLogs ({ wallet, embedded }) {
return (
<>
<Form initial={{ follow: true }}>
<FollowCheckbox
label='follow logs' name='follow' value={follow}
handleChange={setFollow}
/>
</Form>
<div className='d-flex w-100 align-items-center mb-3'>
<Form initial={{ follow: true }}>
<FollowCheckbox
label='follow logs' name='follow' value={follow}
handleChange={setFollow} groupClassName='mb-0'
/>
</Form>
<span
style={{ cursor: 'pointer' }}
className='text-muted fw-bold nav-link' onClick={() => {
showModal(onClose => <DeleteWalletLogsObstacle wallet={wallet} onClose={onClose} />)
}}
>clear
</span>
</div>
<div ref={tableRef} className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
<div className='w-100 text-center'>------ start of logs ------</div>
{logs.length === 0 && <div className='w-100 text-center'>empty</div>}
@ -75,3 +88,34 @@ export default function WalletLogs ({ wallet, embedded }) {
</>
)
}
function DeleteWalletLogsObstacle ({ wallet, onClose }) {
const toaster = useToast()
const { deleteLogs } = useWalletLogger(wallet)
const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?`
return (
<div className='text-center'>
{prompt}
<div className='d-flex justify-center align-items-center mt-3 mx-auto'>
<span style={{ cursor: 'pointer' }} className='d-flex ms-auto text-muted fw-bold nav-link mx-3' onClick={onClose}>cancel</span>
<Button
className='d-flex me-auto mx-3' variant='danger'
onClick={
async () => {
try {
await deleteLogs()
onClose()
toaster.success('deleted wallet logs')
} catch (err) {
console.error(err)
toaster.danger('failed to delete wallet logs')
}
}
}
>delete
</Button>
</div>
</div>
)
}

View File

@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
import { useWalletLogger } from '../logger'
import { Status } from '.'
import { bolt11Tags } from '@/lib/bolt11'
import { Wallet } from '@/lib/constants'
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
@ -67,7 +68,7 @@ export function LNbitsProvider ({ children }) {
const [url, setUrl] = useState('')
const [adminKey, setAdminKey] = useState('')
const [status, setStatus] = useState()
const logger = useWalletLogger('lnbits')
const { logger } = useWalletLogger(Wallet.LNbits)
const name = 'LNbits'
const storageKey = 'webln:provider:lnbits'

View File

@ -7,6 +7,7 @@ import useModal from '../modal'
import { Form, PasswordInput, SubmitButton } from '../form'
import CancelButton from '../cancel-button'
import { Mutex } from 'async-mutex'
import { Wallet } from '@/lib/constants'
const LNCContext = createContext()
const mutex = new Mutex()
@ -32,8 +33,7 @@ function validateNarrowPerms (lnc) {
}
export function LNCProvider ({ children }) {
const name = 'lnc'
const logger = useWalletLogger(name)
const logger = useWalletLogger(Wallet.LNC)
const [config, setConfig] = useState({})
const [lnc, setLNC] = useState()
const [status, setStatus] = useState()
@ -188,7 +188,7 @@ export function LNCProvider ({ children }) {
}, [setStatus, setConfig, logger])
return (
<LNCContext.Provider value={{ name, status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
<LNCContext.Provider value={{ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
{children}
{modal}
</LNCContext.Provider>

View File

@ -6,6 +6,7 @@ import { parseNwcUrl } from '@/lib/url'
import { useWalletLogger } from '../logger'
import { Status } from '.'
import { bolt11Tags } from '@/lib/bolt11'
import { Wallet } from '@/lib/constants'
const NWCContext = createContext()
@ -15,7 +16,7 @@ export function NWCProvider ({ children }) {
const [relayUrl, setRelayUrl] = useState()
const [secret, setSecret] = useState()
const [status, setStatus] = useState()
const logger = useWalletLogger('nwc')
const { logger } = useWalletLogger(Wallet.NWC)
const name = 'NWC'
const storageKey = 'webln:provider:nwc'

View File

@ -131,3 +131,20 @@ export const FAST_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_FAST_POLL_INTER
export const NORMAL_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL)
export const LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL)
export const EXTRA_LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL)
// attached wallets
export const Wallet = {
LND: { logTag: 'lnd', server: true, type: 'LND', field: 'walletLND' },
CLN: { logTag: 'cln', server: true, type: 'CLN', field: 'walletCLN' },
LnAddr: { logTag: 'lnAddr', server: true, type: 'LIGHTNING_ADDRESS', field: 'walletLightningAddress' },
NWC: { logTag: 'nwc', server: false },
LNbits: { logTag: 'lnbits', server: false },
LNC: { logTag: 'lnc', server: false }
}
export const getWalletBy = (key, value) => {
for (const w of Object.values(Wallet)) {
if (w[key] === value) return w
}
throw new Error(`wallet not found: ${key}=${value}`)
}

View File

@ -12,8 +12,9 @@ import { REMOVE_WALLET, UPSERT_WALLET_CLN, WALLET_BY_TYPE } from '@/fragments/wa
import WalletLogs from '@/components/wallet-logs'
import Info from '@/components/info'
import Text from '@/components/text'
import { Wallet } from '@/lib/constants'
const variables = { type: 'CLN' }
const variables = { type: Wallet.CLN.type }
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
export default function CLN ({ ssrData }) {
@ -118,7 +119,7 @@ export default function CLN ({ ssrData }) {
/>
</Form>
<div className='mt-3 w-100'>
<WalletLogs wallet='cln' embedded />
<WalletLogs wallet={Wallet.CLN} embedded />
</div>
</CenterLayout>
)

View File

@ -12,6 +12,7 @@ import { useQuery } from '@apollo/client'
import PageLoading from '@/components/page-loading'
import { LNCCard } from './lnc'
import Link from 'next/link'
import { Wallet as W } from '@/lib/constants'
export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
@ -20,9 +21,9 @@ export default function Wallet ({ ssrData }) {
if (!data && !ssrData) return <PageLoading />
const { wallets } = data || ssrData
const lnd = wallets.find(w => w.type === 'LND')
const lnaddr = wallets.find(w => w.type === 'LIGHTNING_ADDRESS')
const cln = wallets.find(w => w.type === 'CLN')
const lnd = wallets.find(w => w.type === W.LND.type)
const lnaddr = wallets.find(w => w.type === W.LnAddr.type)
const cln = wallets.find(w => w.type === W.CLN.type)
return (
<Layout>

View File

@ -10,8 +10,9 @@ import { useRouter } from 'next/router'
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
import { REMOVE_WALLET, UPSERT_WALLET_LNADDR, WALLET_BY_TYPE } from '@/fragments/wallet'
import WalletLogs from '@/components/wallet-logs'
import { Wallet } from '@/lib/constants'
const variables = { type: 'LIGHTNING_ADDRESS' }
const variables = { type: Wallet.LnAddr.type }
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
export default function LightningAddress ({ ssrData }) {
@ -87,7 +88,7 @@ export default function LightningAddress ({ ssrData }) {
/>
</Form>
<div className='mt-3 w-100'>
<WalletLogs wallet='lnAddr' embedded />
<WalletLogs wallet={Wallet.LnAddr} embedded />
</div>
</CenterLayout>
)

View File

@ -9,6 +9,7 @@ import { useLNbits } from '@/components/webln/lnbits'
import { WalletSecurityBanner } from '@/components/banners'
import { useWebLNConfigurator } from '@/components/webln'
import WalletLogs from '@/components/wallet-logs'
import { Wallet } from '@/lib/constants'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -79,7 +80,7 @@ export default function LNbits () {
/>
</Form>
<div className='mt-3 w-100'>
<WalletLogs wallet='lnbits' embedded />
<WalletLogs wallet={Wallet.LNbits} embedded />
</div>
</CenterLayout>
)

View File

@ -12,6 +12,7 @@ import { XXX_DEFAULT_PASSWORD, useLNC } from '@/components/webln/lnc'
import { lncSchema } from '@/lib/validate'
import { useRouter } from 'next/router'
import { useEffect, useRef } from 'react'
import { Wallet } from '@/lib/constants'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -102,7 +103,7 @@ export default function LNC () {
/>
</Form>
<div className='mt-3 w-100'>
<WalletLogs wallet='lnc' embedded />
<WalletLogs wallet={Wallet.LNC} embedded />
</div>
</CenterLayout>
)

View File

@ -12,8 +12,9 @@ import { REMOVE_WALLET, UPSERT_WALLET_LND, WALLET_BY_TYPE } from '@/fragments/wa
import Info from '@/components/info'
import Text from '@/components/text'
import WalletLogs from '@/components/wallet-logs'
import { Wallet } from '@/lib/constants'
const variables = { type: 'LND' }
const variables = { type: Wallet.LND.type }
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
export default function LND ({ ssrData }) {
@ -118,7 +119,7 @@ export default function LND ({ ssrData }) {
/>
</Form>
<div className='mt-3 w-100'>
<WalletLogs wallet='lnd' embedded />
<WalletLogs wallet={Wallet.LND} embedded />
</div>
</CenterLayout>
)

View File

@ -9,6 +9,7 @@ import { useNWC } from '@/components/webln/nwc'
import { WalletSecurityBanner } from '@/components/banners'
import { useWebLNConfigurator } from '@/components/webln'
import WalletLogs from '@/components/wallet-logs'
import { Wallet } from '@/lib/constants'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -72,7 +73,7 @@ export default function NWC () {
/>
</Form>
<div className='mt-3 w-100'>
<WalletLogs wallet='nwc' embedded />
<WalletLogs wallet={Wallet.NWC} embedded />
</div>
</CenterLayout>
)

View File

@ -0,0 +1,17 @@
/*
Warnings:
- Changed the type of `wallet` on the `WalletLog` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
UPDATE "WalletLog"
SET wallet = CASE
WHEN wallet = 'walletLND' THEN 'LND'
WHEN wallet = 'walletCLN' THEN 'CLN'
WHEN wallet = 'walletLightningAddress' THEN 'LIGHTNING_ADDRESS'
ELSE wallet
END;
-- AlterTable
ALTER TABLE "WalletLog" ALTER COLUMN "wallet" TYPE "WalletType" USING "wallet"::"WalletType";

View File

@ -166,7 +166,7 @@ model WalletLog {
createdAt DateTime @default(now()) @map("created_at")
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
wallet String
wallet WalletType
level LogLevel
message String

View File

@ -3,6 +3,7 @@ import { msatsToSats, satsToMsats } from '@/lib/format'
import { datePivot } from '@/lib/time'
import { createWithdrawal, sendToLnAddr, addWalletLog } from '@/api/resolvers/wallet'
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
import { Wallet } from '@/lib/constants'
export async function autoWithdraw ({ data: { id }, models, lnd }) {
const user = await models.user.findUnique({ where: { id } })
@ -46,15 +47,15 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
for (const wallet of wallets) {
try {
if (wallet.type === 'LND') {
if (wallet.type === Wallet.LND.type) {
await autowithdrawLND(
{ amount, maxFee },
{ models, me: user, lnd })
} else if (wallet.type === 'CLN') {
} else if (wallet.type === Wallet.CLN.type) {
await autowithdrawCLN(
{ amount, maxFee },
{ models, me: user, lnd })
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
} else if (wallet.type === Wallet.LnAddr.type) {
await autowithdrawLNAddr(
{ amount, maxFee },
{ models, me: user, lnd })
@ -66,7 +67,7 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
const details = error[2]?.err?.details || error.message || error.toString?.()
await addWalletLog({
wallet: wallet.type,
wallet,
level: 'ERROR',
message: 'autowithdrawal failed: ' + details
}, { me: user, models })
@ -86,7 +87,7 @@ async function autowithdrawLNAddr (
const wallet = await models.wallet.findFirst({
where: {
userId: me.id,
type: 'LIGHTNING_ADDRESS'
type: Wallet.LnAddr.type
},
include: {
walletLightningAddress: true
@ -109,7 +110,7 @@ async function autowithdrawLND ({ amount, maxFee }, { me, models, lnd }) {
const wallet = await models.wallet.findFirst({
where: {
userId: me.id,
type: 'LND'
type: Wallet.LND.type
},
include: {
walletLND: true
@ -145,7 +146,7 @@ async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
const wallet = await models.wallet.findFirst({
where: {
userId: me.id,
type: 'CLN'
type: Wallet.CLN.type
},
include: {
walletCLN: true