Move wallet components into wallets/
This commit is contained in:
parent
66d7eef617
commit
7be94dcfed
@ -22,7 +22,7 @@ import classNames from 'classnames'
|
|||||||
import SnIcon from '@/svgs/sn.svg'
|
import SnIcon from '@/svgs/sn.svg'
|
||||||
import { useHasNewNotes } from '../use-has-new-notes'
|
import { useHasNewNotes } from '../use-has-new-notes'
|
||||||
import { useWallets } from '@/wallets/index'
|
import { useWallets } from '@/wallets/index'
|
||||||
import { useWalletIndicator } from '@/components/wallet-indicator'
|
import { useWalletIndicator } from '@/wallets/indicator'
|
||||||
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
|
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
|
||||||
import { useShowModal } from '@/components/modal'
|
import { useShowModal } from '@/components/modal'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
|
@ -7,7 +7,7 @@ import AnonIcon from '@/svgs/spy-fill.svg'
|
|||||||
import styles from './footer.module.css'
|
import styles from './footer.module.css'
|
||||||
import canvasStyles from './offcanvas.module.css'
|
import canvasStyles from './offcanvas.module.css'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useWalletIndicator } from '@/components/wallet-indicator'
|
import { useWalletIndicator } from '@/wallets/indicator'
|
||||||
|
|
||||||
export default function OffCanvas ({ me, dropNavKey }) {
|
export default function OffCanvas ({ me, dropNavKey }) {
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
|
@ -1,323 +0,0 @@
|
|||||||
import LogMessage from './log-message'
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import styles from '@/styles/log.module.css'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import { useToast } from './toast'
|
|
||||||
import { useShowModal } from './modal'
|
|
||||||
import { WALLET_LOGS } from '@/fragments/wallet'
|
|
||||||
import { getWalletByType, walletTag } from '@/wallets/common'
|
|
||||||
import { gql, useLazyQuery, useMutation } from '@apollo/client'
|
|
||||||
import { useMe } from './me'
|
|
||||||
import useIndexedDB, { getDbName } from './use-indexeddb'
|
|
||||||
import { SSR } from '@/lib/constants'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
export function WalletLogs ({ wallet, embedded }) {
|
|
||||||
const { logs, setLogs, hasMore, loadMore, loading } = useWalletLogs(wallet)
|
|
||||||
|
|
||||||
const showModal = useShowModal()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='d-flex w-100 align-items-center mb-3'>
|
|
||||||
<span
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
className='text-muted fw-bold nav-link ms-auto' onClick={() => {
|
|
||||||
showModal(onClose => <DeleteWalletLogsObstacle wallet={wallet} setLogs={setLogs} onClose={onClose} />)
|
|
||||||
}}
|
|
||||||
>clear logs
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<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
|
|
||||||
key={i}
|
|
||||||
showWallet={!wallet}
|
|
||||||
{...log}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{loading
|
|
||||||
? <div className='w-100 text-center'>loading...</div>
|
|
||||||
: logs.length === 0 && <div className='w-100 text-center'>empty</div>}
|
|
||||||
{hasMore
|
|
||||||
? <div className='w-100 text-center'><Button onClick={loadMore} size='sm' className='mt-3'>more</Button></div>
|
|
||||||
: <div className='w-100 text-center'>------ start of logs ------</div>}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteWalletLogsObstacle ({ wallet, setLogs, onClose }) {
|
|
||||||
const { deleteLogs } = useWalletLogManager(setLogs)
|
|
||||||
const toaster = useToast()
|
|
||||||
|
|
||||||
let prompt = 'Do you really want to delete all wallet logs?'
|
|
||||||
if (wallet) {
|
|
||||||
prompt = 'Do you really want to delete all logs 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(wallet)
|
|
||||||
onClose()
|
|
||||||
toaster.success('deleted wallet logs')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toaster.danger('failed to delete wallet logs')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const INDICES = [
|
|
||||||
{ name: 'ts', keyPath: 'ts' },
|
|
||||||
{ name: 'wallet_ts', keyPath: ['wallet', 'ts'] }
|
|
||||||
]
|
|
||||||
|
|
||||||
function getWalletLogDbName (userId) {
|
|
||||||
return getDbName(userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useWalletLogDB () {
|
|
||||||
const { me } = useMe()
|
|
||||||
// memoize the idb config to avoid re-creating it on every render
|
|
||||||
const idbConfig = useMemo(() =>
|
|
||||||
({ dbName: getWalletLogDbName(me?.id), storeName: 'wallet_logs', indices: INDICES }), [me?.id])
|
|
||||||
const { add, getPage, clear, error, notSupported } = useIndexedDB(idbConfig)
|
|
||||||
|
|
||||||
return { add, getPage, clear, error, notSupported }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWalletLogManager (setLogs) {
|
|
||||||
const { add, clear, notSupported } = useWalletLogDB()
|
|
||||||
|
|
||||||
const appendLog = useCallback(async (wallet, level, message, context) => {
|
|
||||||
const log = { wallet: walletTag(wallet.def), level, message, ts: +new Date(), context }
|
|
||||||
try {
|
|
||||||
if (notSupported) {
|
|
||||||
console.log('cannot persist wallet log: indexeddb not supported')
|
|
||||||
} else {
|
|
||||||
await add(log)
|
|
||||||
}
|
|
||||||
setLogs?.(prevLogs => [log, ...prevLogs])
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to append wallet log:', error)
|
|
||||||
}
|
|
||||||
}, [add, notSupported])
|
|
||||||
|
|
||||||
const [deleteServerWalletLogs] = useMutation(
|
|
||||||
gql`
|
|
||||||
mutation deleteWalletLogs($wallet: String) {
|
|
||||||
deleteWalletLogs(wallet: $wallet)
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
|
||||||
setLogs?.(logs => logs.filter(l => walletType ? l.wallet !== walletTag(getWalletByType(walletType)) : false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteLogs = useCallback(async (wallet, options) => {
|
|
||||||
if ((!wallet || wallet.def.walletType) && !options?.clientOnly) {
|
|
||||||
await deleteServerWalletLogs({ variables: { wallet: wallet?.def.walletType } })
|
|
||||||
}
|
|
||||||
if (!wallet || wallet.def.sendPayment) {
|
|
||||||
try {
|
|
||||||
const tag = wallet ? walletTag(wallet.def) : null
|
|
||||||
if (notSupported) {
|
|
||||||
console.log('cannot clear wallet logs: indexeddb not supported')
|
|
||||||
} else {
|
|
||||||
await clear('wallet_ts', tag ? window.IDBKeyRange.bound([tag, 0], [tag, Infinity]) : null)
|
|
||||||
}
|
|
||||||
setLogs?.(logs => logs.filter(l => wallet ? l.wallet !== tag : false))
|
|
||||||
} catch (e) {
|
|
||||||
console.error('failed to delete logs', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [clear, deleteServerWalletLogs, setLogs, notSupported])
|
|
||||||
|
|
||||||
return { appendLog, deleteLogs }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|
||||||
const [logs, _setLogs] = useState([])
|
|
||||||
const [page, setPage] = useState(initialPage)
|
|
||||||
const [hasMore, setHasMore] = useState(true)
|
|
||||||
const [cursor, setCursor] = useState(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const latestTimestamp = useRef()
|
|
||||||
const { me } = useMe()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const { getPage, error, notSupported } = useWalletLogDB()
|
|
||||||
const [getWalletLogs] = useLazyQuery(WALLET_LOGS, SSR ? {} : { fetchPolicy: 'cache-and-network' })
|
|
||||||
|
|
||||||
const setLogs = useCallback((action) => {
|
|
||||||
_setLogs(action)
|
|
||||||
// action can be a React state dispatch function
|
|
||||||
const newLogs = typeof action === 'function' ? action(logs) : action
|
|
||||||
// make sure 'more' button is removed if logs were deleted
|
|
||||||
if (newLogs.length === 0) setHasMore(false)
|
|
||||||
latestTimestamp.current = newLogs[0]?.ts
|
|
||||||
}, [logs, _setLogs, setHasMore])
|
|
||||||
|
|
||||||
const loadLogsPage = useCallback(async (page, pageSize, walletDef, variables = {}) => {
|
|
||||||
try {
|
|
||||||
let result = { data: [], hasMore: false }
|
|
||||||
if (notSupported) {
|
|
||||||
console.log('cannot get client wallet logs: indexeddb not supported')
|
|
||||||
} else {
|
|
||||||
const indexName = walletDef ? 'wallet_ts' : 'ts'
|
|
||||||
const query = walletDef ? window.IDBKeyRange.bound([walletTag(walletDef), -Infinity], [walletTag(walletDef), Infinity]) : null
|
|
||||||
|
|
||||||
result = await getPage(page, pageSize, indexName, query, 'prev')
|
|
||||||
// if given wallet has no walletType it means logs are only stored in local IDB
|
|
||||||
if (walletDef && !walletDef.walletType) {
|
|
||||||
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, error } = await getWalletLogs({
|
|
||||||
variables: {
|
|
||||||
type: walletDef?.walletType,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
cursor,
|
|
||||||
...variables
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('failed to query wallet logs:', error)
|
|
||||||
return { data: [], hasMore: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLogs = data.walletLogs.entries.map(({ createdAt, wallet: walletType, ...log }) => ({
|
|
||||||
ts: +new Date(createdAt),
|
|
||||||
wallet: walletType ? walletTag(getWalletByType(walletType)) : 'system',
|
|
||||||
...log,
|
|
||||||
// required to resolve recv status
|
|
||||||
context: {
|
|
||||||
recv: true,
|
|
||||||
status: !!log.context?.bolt11 && ['warn', 'error', 'success'].includes(log.level.toLowerCase()),
|
|
||||||
...log.context
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
const combinedLogs = uniqueSort([...result.data, ...newLogs])
|
|
||||||
|
|
||||||
setCursor(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: [], hasMore: false }
|
|
||||||
}
|
|
||||||
}, [getPage, setCursor, cursor, notSupported])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('IndexedDB error:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMore = useCallback(async () => {
|
|
||||||
if (hasMore) {
|
|
||||||
setLoading(true)
|
|
||||||
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
|
|
||||||
setLogs(prevLogs => uniqueSort([...prevLogs, ...result.data]))
|
|
||||||
setHasMore(result.hasMore)
|
|
||||||
setPage(prevPage => prevPage + 1)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [setLogs, loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
|
|
||||||
|
|
||||||
const loadNew = useCallback(async () => {
|
|
||||||
const latestTs = latestTimestamp.current
|
|
||||||
const variables = { from: latestTs?.toString(), to: null }
|
|
||||||
const result = await loadLogsPage(1, logsPerPage, wallet?.def, variables)
|
|
||||||
setLoading(false)
|
|
||||||
_setLogs(prevLogs => uniqueSort([...result.data, ...prevLogs]))
|
|
||||||
if (!latestTs) {
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}, [wallet?.def, loadLogsPage])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// only fetch new logs if we are on a page that uses logs
|
|
||||||
const needLogs = router.asPath.startsWith('/wallets')
|
|
||||||
if (!me || !needLogs) return
|
|
||||||
|
|
||||||
let timeout
|
|
||||||
let stop = false
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
await loadNew().catch(console.error)
|
|
||||||
if (!stop) timeout = setTimeout(poll, 1_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout = setTimeout(poll, 1_000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stop = true
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
|
||||||
}, [me?.id, router.pathname, loadNew])
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
|
|||||||
import { Form, ClientInput, PasswordInput, CheckboxGroup, Checkbox } from '@/components/form'
|
import { Form, ClientInput, PasswordInput, CheckboxGroup, Checkbox } from '@/components/form'
|
||||||
import { CenterLayout } from '@/components/layout'
|
import { CenterLayout } from '@/components/layout'
|
||||||
import { WalletSecurityBanner } from '@/components/banners'
|
import { WalletSecurityBanner } from '@/components/banners'
|
||||||
import { WalletLogs } from '@/components/wallet-logger'
|
import { WalletLogs } from '@/wallets/logger'
|
||||||
import { useToast } from '@/components/toast'
|
import { useToast } from '@/components/toast'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useWallet } from '@/wallets/index'
|
import { useWallet } from '@/wallets/index'
|
||||||
@ -11,14 +11,14 @@ import Text from '@/components/text'
|
|||||||
import { autowithdrawInitial, AutowithdrawSettings } from '@/components/autowithdraw-shared'
|
import { autowithdrawInitial, AutowithdrawSettings } from '@/components/autowithdraw-shared'
|
||||||
import { canReceive, canSend, isConfigured } from '@/wallets/common'
|
import { canReceive, canSend, isConfigured } from '@/wallets/common'
|
||||||
import { SSR } from '@/lib/constants'
|
import { SSR } from '@/lib/constants'
|
||||||
import WalletButtonBar from '@/components/wallet-buttonbar'
|
import WalletButtonBar from '@/wallets/buttonbar'
|
||||||
import { useWalletConfigurator } from '@/wallets/config'
|
import { useWalletConfigurator } from '@/wallets/config'
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
import validateWallet from '@/wallets/validate'
|
import validateWallet from '@/wallets/validate'
|
||||||
import { ValidationError } from 'yup'
|
import { ValidationError } from 'yup'
|
||||||
import { useFormikContext } from 'formik'
|
import { useFormikContext } from 'formik'
|
||||||
import { useWalletImage } from '@/components/wallet-image'
|
import { useWalletImage } from '@/wallets/image'
|
||||||
import styles from '@/styles/wallet.module.css'
|
import styles from '@/styles/wallet.module.css'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
@ -5,14 +5,14 @@ import Link from 'next/link'
|
|||||||
import { useWallets } from '@/wallets/index'
|
import { useWallets } from '@/wallets/index'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useIsClient } from '@/components/use-client'
|
import { useIsClient } from '@/components/use-client'
|
||||||
import WalletCard from '@/components/wallet-card'
|
import WalletCard from '@/wallets/card'
|
||||||
import { useToast } from '@/components/toast'
|
import { useToast } from '@/components/toast'
|
||||||
import BootstrapForm from 'react-bootstrap/Form'
|
import BootstrapForm from 'react-bootstrap/Form'
|
||||||
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
|
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
|
||||||
import SendIcon from '@/svgs/arrow-right-up-line.svg'
|
import SendIcon from '@/svgs/arrow-right-up-line.svg'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { supportsReceive, supportsSend } from '@/wallets/common'
|
import { supportsReceive, supportsSend } from '@/wallets/common'
|
||||||
import { useWalletIndicator } from '@/components/wallet-indicator'
|
import { useWalletIndicator } from '@/wallets/indicator'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CenterLayout } from '@/components/layout'
|
import { CenterLayout } from '@/components/layout'
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||||
import { WalletLogs } from '@/components/wallet-logger'
|
import { WalletLogs } from '@/wallets/logger'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: null })
|
export const getServerSideProps = getGetServerSideProps({ query: null })
|
||||||
|
|
||||||
|
@ -168,7 +168,7 @@ How this validation is implemented depends heavily on the wallet. For example, f
|
|||||||
|
|
||||||
This function must throw an error if the configuration was found to be invalid.
|
This function must throw an error if the configuration was found to be invalid.
|
||||||
|
|
||||||
The `context` argument is an object. It makes the wallet logger for this wallet as returned by `useWalletLogger` available under `context.logger`. See [components/wallet-logger.js](../components/wallet-logger.js).
|
The `context` argument is an object. It makes the wallet logger for this wallet as returned by `useWalletLogger` available under `context.logger`. See [wallets/logger.js](../wallets/logger.js).
|
||||||
|
|
||||||
- `sendPayment: async (bolt11: string, config, context) => Promise<string>`
|
- `sendPayment: async (bolt11: string, config, context) => Promise<string>`
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import CancelButton from './cancel-button'
|
import CancelButton from '@/components/cancel-button'
|
||||||
import { SubmitButton } from './form'
|
import { SubmitButton } from '@/components/form'
|
||||||
import { isConfigured } from '@/wallets/common'
|
import { isConfigured } from '@/wallets/common'
|
||||||
|
|
||||||
export default function WalletButtonBar ({
|
export default function WalletButtonBar ({
|
@ -7,9 +7,9 @@ import { Status, isConfigured } from '@/wallets/common'
|
|||||||
import DraggableIcon from '@/svgs/draggable.svg'
|
import DraggableIcon from '@/svgs/draggable.svg'
|
||||||
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
|
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
|
||||||
import SendIcon from '@/svgs/arrow-right-up-line.svg'
|
import SendIcon from '@/svgs/arrow-right-up-line.svg'
|
||||||
import { useWalletImage } from '@/components/wallet-image'
|
import { useWalletImage } from '@/wallets/image'
|
||||||
import { useWalletStatus, statusToClass } from '@/components/wallet-status'
|
import { useWalletStatus, statusToClass } from '@/wallets/status'
|
||||||
import { useWalletSupport } from '@/components/wallet-support'
|
import { useWalletSupport } from '@/wallets/support'
|
||||||
|
|
||||||
export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnter, onDragEnd, onTouchStart, sourceIndex, targetIndex, index }) {
|
export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnter, onDragEnd, onTouchStart, sourceIndex, targetIndex, index }) {
|
||||||
const image = useWalletImage(wallet)
|
const image = useWalletImage(wallet)
|
@ -1,8 +1,23 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo, useState, useEffect, useRef } from 'react'
|
||||||
import { decode as bolt11Decode } from 'bolt11'
|
import { decode as bolt11Decode } from 'bolt11'
|
||||||
import { formatMsats } from '@/lib/format'
|
import { formatMsats } from '@/lib/format'
|
||||||
import { walletTag } from '@/wallets/common'
|
import { walletTag, getWalletByType } from '@/wallets/common'
|
||||||
import { useWalletLogManager } from '@/components/wallet-logger'
|
import { useMe } from '@/components/me'
|
||||||
|
import useIndexedDB, { getDbName } from '@/components/use-indexeddb'
|
||||||
|
import { useShowModal } from '@/components/modal'
|
||||||
|
import LogMessage from '@/components/log-message'
|
||||||
|
import { useToast } from '@/components/toast'
|
||||||
|
import { useMutation, useLazyQuery, gql } from '@apollo/client'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { WALLET_LOGS } from '@/fragments/wallet'
|
||||||
|
import { SSR } from '@/lib/constants'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
import styles from '@/styles/log.module.css'
|
||||||
|
|
||||||
|
const INDICES = [
|
||||||
|
{ name: 'ts', keyPath: 'ts' },
|
||||||
|
{ name: 'wallet_ts', keyPath: ['wallet', 'ts'] }
|
||||||
|
]
|
||||||
|
|
||||||
export function useWalletLoggerFactory () {
|
export function useWalletLoggerFactory () {
|
||||||
const { appendLog } = useWalletLogManager()
|
const { appendLog } = useWalletLogManager()
|
||||||
@ -43,3 +58,303 @@ export function useWalletLogger (wallet) {
|
|||||||
const factory = useWalletLoggerFactory()
|
const factory = useWalletLoggerFactory()
|
||||||
return factory(wallet)
|
return factory(wallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function WalletLogs ({ wallet, embedded }) {
|
||||||
|
const { logs, setLogs, hasMore, loadMore, loading } = useWalletLogs(wallet)
|
||||||
|
|
||||||
|
const showModal = useShowModal()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='d-flex w-100 align-items-center mb-3'>
|
||||||
|
<span
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
className='text-muted fw-bold nav-link ms-auto' onClick={() => {
|
||||||
|
showModal(onClose => <DeleteWalletLogsObstacle wallet={wallet} setLogs={setLogs} onClose={onClose} />)
|
||||||
|
}}
|
||||||
|
>clear logs
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<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
|
||||||
|
key={i}
|
||||||
|
showWallet={!wallet}
|
||||||
|
{...log}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{loading
|
||||||
|
? <div className='w-100 text-center'>loading...</div>
|
||||||
|
: logs.length === 0 && <div className='w-100 text-center'>empty</div>}
|
||||||
|
{hasMore
|
||||||
|
? <div className='w-100 text-center'><Button onClick={loadMore} size='sm' className='mt-3'>more</Button></div>
|
||||||
|
: <div className='w-100 text-center'>------ start of logs ------</div>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteWalletLogsObstacle ({ wallet, setLogs, onClose }) {
|
||||||
|
const { deleteLogs } = useWalletLogManager(setLogs)
|
||||||
|
const toaster = useToast()
|
||||||
|
|
||||||
|
let prompt = 'Do you really want to delete all wallet logs?'
|
||||||
|
if (wallet) {
|
||||||
|
prompt = 'Do you really want to delete all logs 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(wallet)
|
||||||
|
onClose()
|
||||||
|
toaster.success('deleted wallet logs')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to delete wallet logs')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWalletLogManager (setLogs) {
|
||||||
|
const { add, clear, notSupported } = useWalletLogDB()
|
||||||
|
|
||||||
|
const appendLog = useCallback(async (wallet, level, message, context) => {
|
||||||
|
const log = { wallet: walletTag(wallet.def), level, message, ts: +new Date(), context }
|
||||||
|
try {
|
||||||
|
if (notSupported) {
|
||||||
|
console.log('cannot persist wallet log: indexeddb not supported')
|
||||||
|
} else {
|
||||||
|
await add(log)
|
||||||
|
}
|
||||||
|
setLogs?.(prevLogs => [log, ...prevLogs])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to append wallet log:', error)
|
||||||
|
}
|
||||||
|
}, [add, notSupported])
|
||||||
|
|
||||||
|
const [deleteServerWalletLogs] = useMutation(
|
||||||
|
gql`
|
||||||
|
mutation deleteWalletLogs($wallet: String) {
|
||||||
|
deleteWalletLogs(wallet: $wallet)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
||||||
|
setLogs?.(logs => logs.filter(l => walletType ? l.wallet !== walletTag(getWalletByType(walletType)) : false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteLogs = useCallback(async (wallet, options) => {
|
||||||
|
if ((!wallet || wallet.def.walletType) && !options?.clientOnly) {
|
||||||
|
await deleteServerWalletLogs({ variables: { wallet: wallet?.def.walletType } })
|
||||||
|
}
|
||||||
|
if (!wallet || wallet.def.sendPayment) {
|
||||||
|
try {
|
||||||
|
const tag = wallet ? walletTag(wallet.def) : null
|
||||||
|
if (notSupported) {
|
||||||
|
console.log('cannot clear wallet logs: indexeddb not supported')
|
||||||
|
} else {
|
||||||
|
await clear('wallet_ts', tag ? window.IDBKeyRange.bound([tag, 0], [tag, Infinity]) : null)
|
||||||
|
}
|
||||||
|
setLogs?.(logs => logs.filter(l => wallet ? l.wallet !== tag : false))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('failed to delete logs', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [clear, deleteServerWalletLogs, setLogs, notSupported])
|
||||||
|
|
||||||
|
return { appendLog, deleteLogs }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
||||||
|
const [logs, _setLogs] = useState([])
|
||||||
|
const [page, setPage] = useState(initialPage)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [cursor, setCursor] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const latestTimestamp = useRef()
|
||||||
|
const { me } = useMe()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { getPage, error, notSupported } = useWalletLogDB()
|
||||||
|
const [getWalletLogs] = useLazyQuery(WALLET_LOGS, SSR ? {} : { fetchPolicy: 'cache-and-network' })
|
||||||
|
|
||||||
|
const setLogs = useCallback((action) => {
|
||||||
|
_setLogs(action)
|
||||||
|
// action can be a React state dispatch function
|
||||||
|
const newLogs = typeof action === 'function' ? action(logs) : action
|
||||||
|
// make sure 'more' button is removed if logs were deleted
|
||||||
|
if (newLogs.length === 0) setHasMore(false)
|
||||||
|
latestTimestamp.current = newLogs[0]?.ts
|
||||||
|
}, [logs, _setLogs, setHasMore])
|
||||||
|
|
||||||
|
const loadLogsPage = useCallback(async (page, pageSize, walletDef, variables = {}) => {
|
||||||
|
try {
|
||||||
|
let result = { data: [], hasMore: false }
|
||||||
|
if (notSupported) {
|
||||||
|
console.log('cannot get client wallet logs: indexeddb not supported')
|
||||||
|
} else {
|
||||||
|
const indexName = walletDef ? 'wallet_ts' : 'ts'
|
||||||
|
const query = walletDef ? window.IDBKeyRange.bound([walletTag(walletDef), -Infinity], [walletTag(walletDef), Infinity]) : null
|
||||||
|
|
||||||
|
result = await getPage(page, pageSize, indexName, query, 'prev')
|
||||||
|
// if given wallet has no walletType it means logs are only stored in local IDB
|
||||||
|
if (walletDef && !walletDef.walletType) {
|
||||||
|
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,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
cursor,
|
||||||
|
...variables
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const newLogs = data.walletLogs.entries.map(({ createdAt, wallet: walletType, ...log }) => ({
|
||||||
|
ts: +new Date(createdAt),
|
||||||
|
wallet: walletTag(getWalletByType(walletType)),
|
||||||
|
...log,
|
||||||
|
// required to resolve recv status
|
||||||
|
context: {
|
||||||
|
recv: true,
|
||||||
|
status: !!log.context?.bolt11 && ['warn', 'error', 'success'].includes(log.level.toLowerCase()),
|
||||||
|
...log.context
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
const combinedLogs = uniqueSort([...result.data, ...newLogs])
|
||||||
|
|
||||||
|
setCursor(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: [], hasMore: false }
|
||||||
|
}
|
||||||
|
}, [getPage, setCursor, cursor, notSupported])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('IndexedDB error:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (hasMore) {
|
||||||
|
setLoading(true)
|
||||||
|
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
|
||||||
|
setLogs(prevLogs => uniqueSort([...prevLogs, ...result.data]))
|
||||||
|
setHasMore(result.hasMore)
|
||||||
|
setPage(prevPage => prevPage + 1)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [setLogs, loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
|
||||||
|
|
||||||
|
const loadNew = useCallback(async () => {
|
||||||
|
const latestTs = latestTimestamp.current
|
||||||
|
const variables = { from: latestTs?.toString(), to: null }
|
||||||
|
const result = await loadLogsPage(1, logsPerPage, wallet?.def, variables)
|
||||||
|
setLoading(false)
|
||||||
|
_setLogs(prevLogs => uniqueSort([...result.data, ...prevLogs]))
|
||||||
|
if (!latestTs) {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}, [wallet?.def, loadLogsPage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// only fetch new logs if we are on a page that uses logs
|
||||||
|
const needLogs = router.asPath.startsWith('/wallets')
|
||||||
|
if (!me || !needLogs) return
|
||||||
|
|
||||||
|
let timeout
|
||||||
|
let stop = false
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
await loadNew().catch(console.error)
|
||||||
|
if (!stop) timeout = setTimeout(poll, 1_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout = setTimeout(poll, 1_000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stop = true
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [me?.id, router.pathname, loadNew])
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWalletLogDbName (userId) {
|
||||||
|
return getDbName(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useWalletLogDB () {
|
||||||
|
const { me } = useMe()
|
||||||
|
// memoize the idb config to avoid re-creating it on every render
|
||||||
|
const idbConfig = useMemo(() =>
|
||||||
|
({ dbName: getWalletLogDbName(me?.id), storeName: 'wallet_logs', indices: INDICES }), [me?.id])
|
||||||
|
const { add, getPage, clear, error, notSupported } = useIndexedDB(idbConfig)
|
||||||
|
|
||||||
|
return { add, getPage, clear, error, notSupported }
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { canReceive, canSend, isConfigured, Status } from '@/wallets/common'
|
import { canReceive, canSend, isConfigured, Status } from '@/wallets/common'
|
||||||
import { useWalletLogs } from '@/components/wallet-logger'
|
import { useWalletLogs } from '@/wallets/logger'
|
||||||
import styles from '@/styles/wallet.module.css'
|
import styles from '@/styles/wallet.module.css'
|
||||||
|
|
||||||
export function useWalletStatus (wallet) {
|
export function useWalletStatus (wallet) {
|
Loading…
x
Reference in New Issue
Block a user