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 { useHasNewNotes } from '../use-has-new-notes'
 | 
			
		||||
import { useWallets } from '@/wallets/index'
 | 
			
		||||
import { useWalletIndicator } from '@/components/wallet-indicator'
 | 
			
		||||
import { useWalletIndicator } from '@/wallets/indicator'
 | 
			
		||||
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
 | 
			
		||||
import { useShowModal } from '@/components/modal'
 | 
			
		||||
import { numWithUnits } from '@/lib/format'
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import AnonIcon from '@/svgs/spy-fill.svg'
 | 
			
		||||
import styles from './footer.module.css'
 | 
			
		||||
import canvasStyles from './offcanvas.module.css'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import { useWalletIndicator } from '@/components/wallet-indicator'
 | 
			
		||||
import { useWalletIndicator } from '@/wallets/indicator'
 | 
			
		||||
 | 
			
		||||
export default function OffCanvas ({ me, dropNavKey }) {
 | 
			
		||||
  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 { CenterLayout } from '@/components/layout'
 | 
			
		||||
import { WalletSecurityBanner } from '@/components/banners'
 | 
			
		||||
import { WalletLogs } from '@/components/wallet-logger'
 | 
			
		||||
import { WalletLogs } from '@/wallets/logger'
 | 
			
		||||
import { useToast } from '@/components/toast'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { useWallet } from '@/wallets/index'
 | 
			
		||||
@ -11,14 +11,14 @@ import Text from '@/components/text'
 | 
			
		||||
import { autowithdrawInitial, AutowithdrawSettings } from '@/components/autowithdraw-shared'
 | 
			
		||||
import { canReceive, canSend, isConfigured } from '@/wallets/common'
 | 
			
		||||
import { SSR } from '@/lib/constants'
 | 
			
		||||
import WalletButtonBar from '@/components/wallet-buttonbar'
 | 
			
		||||
import WalletButtonBar from '@/wallets/buttonbar'
 | 
			
		||||
import { useWalletConfigurator } from '@/wallets/config'
 | 
			
		||||
import { useCallback, useMemo } from 'react'
 | 
			
		||||
import { useMe } from '@/components/me'
 | 
			
		||||
import validateWallet from '@/wallets/validate'
 | 
			
		||||
import { ValidationError } from 'yup'
 | 
			
		||||
import { useFormikContext } from 'formik'
 | 
			
		||||
import { useWalletImage } from '@/components/wallet-image'
 | 
			
		||||
import { useWalletImage } from '@/wallets/image'
 | 
			
		||||
import styles from '@/styles/wallet.module.css'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
			
		||||
 | 
			
		||||
@ -5,14 +5,14 @@ import Link from 'next/link'
 | 
			
		||||
import { useWallets } from '@/wallets/index'
 | 
			
		||||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useIsClient } from '@/components/use-client'
 | 
			
		||||
import WalletCard from '@/components/wallet-card'
 | 
			
		||||
import WalletCard from '@/wallets/card'
 | 
			
		||||
import { useToast } from '@/components/toast'
 | 
			
		||||
import BootstrapForm from 'react-bootstrap/Form'
 | 
			
		||||
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
 | 
			
		||||
import SendIcon from '@/svgs/arrow-right-up-line.svg'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { supportsReceive, supportsSend } from '@/wallets/common'
 | 
			
		||||
import { useWalletIndicator } from '@/components/wallet-indicator'
 | 
			
		||||
import { useWalletIndicator } from '@/wallets/indicator'
 | 
			
		||||
import { Button } from 'react-bootstrap'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { CenterLayout } from '@/components/layout'
 | 
			
		||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
			
		||||
import { WalletLogs } from '@/components/wallet-logger'
 | 
			
		||||
import { WalletLogs } from '@/wallets/logger'
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
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>`
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { Button } from 'react-bootstrap'
 | 
			
		||||
import CancelButton from './cancel-button'
 | 
			
		||||
import { SubmitButton } from './form'
 | 
			
		||||
import CancelButton from '@/components/cancel-button'
 | 
			
		||||
import { SubmitButton } from '@/components/form'
 | 
			
		||||
import { isConfigured } from '@/wallets/common'
 | 
			
		||||
 | 
			
		||||
export default function WalletButtonBar ({
 | 
			
		||||
@ -7,9 +7,9 @@ import { Status, isConfigured } from '@/wallets/common'
 | 
			
		||||
import DraggableIcon from '@/svgs/draggable.svg'
 | 
			
		||||
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
 | 
			
		||||
import SendIcon from '@/svgs/arrow-right-up-line.svg'
 | 
			
		||||
import { useWalletImage } from '@/components/wallet-image'
 | 
			
		||||
import { useWalletStatus, statusToClass } from '@/components/wallet-status'
 | 
			
		||||
import { useWalletSupport } from '@/components/wallet-support'
 | 
			
		||||
import { useWalletImage } from '@/wallets/image'
 | 
			
		||||
import { useWalletStatus, statusToClass } from '@/wallets/status'
 | 
			
		||||
import { useWalletSupport } from '@/wallets/support'
 | 
			
		||||
 | 
			
		||||
export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnter, onDragEnd, onTouchStart, sourceIndex, targetIndex, index }) {
 | 
			
		||||
  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 { formatMsats } from '@/lib/format'
 | 
			
		||||
import { walletTag } from '@/wallets/common'
 | 
			
		||||
import { useWalletLogManager } from '@/components/wallet-logger'
 | 
			
		||||
import { walletTag, getWalletByType } from '@/wallets/common'
 | 
			
		||||
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 () {
 | 
			
		||||
  const { appendLog } = useWalletLogManager()
 | 
			
		||||
@ -43,3 +58,303 @@ export function useWalletLogger (wallet) {
 | 
			
		||||
  const factory = useWalletLoggerFactory()
 | 
			
		||||
  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 { useWalletLogs } from '@/components/wallet-logger'
 | 
			
		||||
import { useWalletLogs } from '@/wallets/logger'
 | 
			
		||||
import styles from '@/styles/wallet.module.css'
 | 
			
		||||
 | 
			
		||||
export function useWalletStatus (wallet) {
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user