Add wallet debug page (#2301)

* Add wallet debug page

* Show key hash information

* Show last key update

* Show last wallet update

* Show last device key update
This commit is contained in:
ekzyis 2025-07-21 22:39:09 +02:00 committed by GitHub
parent 6b440cfdf3
commit faa26ec68f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 162 additions and 13 deletions

View File

@ -202,6 +202,7 @@ export default gql`
withdrawMaxFeeDefault: Int! withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int autoWithdrawThreshold: Int
vaultKeyHash: String vaultKeyHash: String
vaultKeyHashUpdatedAt: Date
walletsUpdatedAt: Date walletsUpdatedAt: Date
} }

View File

@ -48,6 +48,7 @@ ${STREAK_FIELDS}
wildWestMode wildWestMode
disableFreebies disableFreebies
vaultKeyHash vaultKeyHash
vaultKeyHashUpdatedAt
walletsUpdatedAt walletsUpdatedAt
showPassphrase showPassphrase
} }

View File

@ -250,3 +250,13 @@ export const truncateString = (str, maxLength, suffix = ' ...') => {
return result return result
} }
export function formatBytes (bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let index = 0
while (bytes >= 1024 && index < units.length - 1) {
bytes /= 1024
index++
}
return `${bytes.toFixed(2)} ${units[index]}`
}

15
pages/wallets/debug.js Normal file
View File

@ -0,0 +1,15 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import { WalletLayout, WalletLayoutHeader, WalletDebugSettings } from '@/wallets/client/components'
export const getServerSideProps = getGetServerSideProps({})
export default function WalletDebug () {
return (
<WalletLayout>
<div className='py-5 mx-auto w-100' style={{ maxWidth: '600px' }}>
<WalletLayoutHeader>wallet debug</WalletLayoutHeader>
<WalletDebugSettings />
</div>
</WalletLayout>
)
}

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "vaultKeyHashUpdatedAt" TIMESTAMP(3);

View File

@ -145,6 +145,7 @@ model User {
oneDayReferrals OneDayReferral[] @relation("OneDayReferral_referrer") oneDayReferrals OneDayReferral[] @relation("OneDayReferral_referrer")
oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees") oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees")
vaultKeyHash String @default("") vaultKeyHash String @default("")
vaultKeyHashUpdatedAt DateTime?
showPassphrase Boolean @default(true) showPassphrase Boolean @default(true)
walletsUpdatedAt DateTime? walletsUpdatedAt DateTime?
proxyReceive Boolean @default(true) proxyReceive Boolean @default(true)

View File

@ -0,0 +1,86 @@
import { formatBytes } from '@/lib/format'
import { useEffect, useState } from 'react'
import { useKeyHash, useKeyUpdatedAt } from '@/wallets/client/context'
import { useRemoteKeyHash, useRemoteKeyHashUpdatedAt, useWalletsUpdatedAt } from '@/wallets/client/hooks'
import { timeSince } from '@/lib/time'
export function WalletDebugSettings () {
const localKeyHash = useKeyHash()
const localKeyUpdatedAt = useKeyUpdatedAt()
const remoteKeyHash = useRemoteKeyHash()
const remoteKeyHashUpdatedAt = useRemoteKeyHashUpdatedAt()
const walletsUpdatedAt = useWalletsUpdatedAt()
const [persistent, setPersistent] = useState(null)
const [quota, setQuota] = useState(null)
const [usage, setUsage] = useState(null)
useEffect(() => {
async function init () {
try {
const persistent = await navigator.storage.persisted()
setPersistent(persistent)
} catch (err) {
console.error('failed to check persistent storage:', err)
}
try {
const estimate = await navigator.storage.estimate()
setQuota(estimate.quota)
setUsage(estimate.usage)
} catch (err) {
console.error('failed to get estimate:', err)
}
}
init()
}, [])
return (
<div className='container text-muted mt-5'>
<div className='row'>
<div className='col text-nowrap'>persistent storage:</div>
<div className='col'>{persistent !== null ? persistent?.toString() : 'unknown'}</div>
</div>
<div className='row'>
<div className='col text-nowrap'>storage quota:</div>
<div className='col'>{quota !== null ? formatBytes(quota) : 'unknown'}</div>
</div>
<div className='row'>
<div className='col text-nowrap'>storage usage:</div>
<div className='col'>{usage !== null ? formatBytes(usage) : 'unknown'}</div>
</div>
<div className='row'>
<div className='col text-nowrap'>storage remaining:</div>
<div className='col'>{usage !== null && quota !== null ? formatBytes(quota - usage) : 'unknown'}</div>
</div>
<div className='row'>
<div className='col text-nowrap'>device key hash:</div>
<div className='col'>{localKeyHash ? shortHash(localKeyHash) : 'unknown'}</div>
</div>
<div className='row'>
<div className='col text-nowrap'>server key hash:</div>
<div className='col'>{remoteKeyHash ? shortHash(remoteKeyHash) : 'unknown'}</div>
</div>
<div className='row'>
<div className='col text-nowrap'>last device key update:</div>
<div className='col' suppressHydrationWarning>
{localKeyUpdatedAt ? timeSince(localKeyUpdatedAt) : 'unknown'}
</div>
</div>
<div className='row'>
<div className='col text-nowrap'>last server key update:</div>
<div className='col' suppressHydrationWarning>
{remoteKeyHashUpdatedAt ? timeSince(new Date(remoteKeyHashUpdatedAt).getTime()) : 'unknown'}
</div>
</div>
<div className='row'>
<div className='col text-nowrap'>last wallet update:</div>
<div className='col' suppressHydrationWarning>
{walletsUpdatedAt ? timeSince(new Date(walletsUpdatedAt).getTime()) : 'unknown'}
</div>
</div>
</div>
)
}
function shortHash (hash) {
return hash.slice(0, 6) + '...' + hash.slice(-6)
}

View File

@ -4,3 +4,4 @@ export * from './forms'
export * from './layout' export * from './layout'
export * from './passphrase' export * from './passphrase'
export * from './logger' export * from './logger'
export * from './debug'

View File

@ -160,7 +160,7 @@ export function useKeyInit () {
const { key: randomKey, hash: randomHash } = await generateRandomKey() const { key: randomKey, hash: randomHash } = await generateRandomKey()
// run read and write in one transaction to avoid race conditions // run read and write in one transaction to avoid race conditions
const { key, hash } = await new Promise((resolve, reject) => { const { key, hash, updatedAt } = await new Promise((resolve, reject) => {
const tx = db.transaction('vault', 'readwrite') const tx = db.transaction('vault', 'readwrite')
const read = tx.objectStore('vault').get('key') const read = tx.objectStore('vault').get('key')
@ -180,7 +180,8 @@ export function useKeyInit () {
} }
// no key found, write and return generated random key // no key found, write and return generated random key
const write = tx.objectStore('vault').put({ key: randomKey, hash: randomHash }, 'key') const updatedAt = Date.now()
const write = tx.objectStore('vault').put({ key: randomKey, hash: randomHash, updatedAt }, 'key')
write.onerror = () => { write.onerror = () => {
reject(write.error) reject(write.error)
@ -188,12 +189,12 @@ export function useKeyInit () {
write.onsuccess = (event) => { write.onsuccess = (event) => {
// return key+hash we just wrote to db // return key+hash we just wrote to db
resolve({ key: randomKey, hash: randomHash }) resolve({ key: randomKey, hash: randomHash, updatedAt })
} }
} }
}) })
await setKey({ key, hash }) await setKey({ key, hash, updatedAt }, { updateDb: false })
} catch (err) { } catch (err) {
console.error('key init failed:', err) console.error('key init failed:', err)
} }

View File

@ -41,6 +41,11 @@ export function useKeyHash () {
return keyHash return keyHash
} }
export function useKeyUpdatedAt () {
const { keyUpdatedAt } = useContext(WalletsContext)
return keyUpdatedAt
}
export function useKeyError () { export function useKeyError () {
const { keyError } = useContext(WalletsContext) const { keyError } = useContext(WalletsContext)
return keyError return keyError
@ -54,6 +59,7 @@ export default function WalletsProvider ({ children }) {
templates: [], templates: [],
key: null, key: null,
keyHash: null, keyHash: null,
keyUpdatedAt: null,
keyError: null keyError: null
}) })

View File

@ -40,7 +40,8 @@ export default function reducer (state, action) {
return { return {
...state, ...state,
key: action.key, key: action.key,
keyHash: action.hash keyHash: action.hash,
keyUpdatedAt: action.updatedAt
} }
case WRONG_KEY: case WRONG_KEY:
return { return {

View File

@ -42,10 +42,13 @@ export function useSetKey () {
const dispatch = useWalletsDispatch() const dispatch = useWalletsDispatch()
const updateKeyHash = useUpdateKeyHash() const updateKeyHash = useUpdateKeyHash()
return useCallback(async ({ key, hash }) => { return useCallback(async ({ key, hash, updatedAt }, { updateDb = true } = {}) => {
await set('vault', 'key', { key, hash }) if (updateDb) {
updatedAt = updatedAt ?? Date.now()
await set('vault', 'key', { key, hash, updatedAt })
}
await updateKeyHash(hash) await updateKeyHash(hash)
dispatch({ type: SET_KEY, key, hash }) dispatch({ type: SET_KEY, key, hash, updatedAt })
}, [set, dispatch, updateKeyHash]) }, [set, dispatch, updateKeyHash])
} }
@ -86,6 +89,11 @@ export function useRemoteKeyHash () {
return me?.privates?.vaultKeyHash return me?.privates?.vaultKeyHash
} }
export function useRemoteKeyHashUpdatedAt () {
const { me } = useMe()
return me?.privates?.vaultKeyHashUpdatedAt
}
export function useIsWrongKey () { export function useIsWrongKey () {
const localHash = useKeyHash() const localHash = useKeyHash()
const remoteHash = useRemoteKeyHash() const remoteHash = useRemoteKeyHash()

View File

@ -22,7 +22,7 @@ import {
UPDATE_KEY_HASH UPDATE_KEY_HASH
} from '@/wallets/client/fragments' } from '@/wallets/client/fragments'
import { useApolloClient, useMutation, useQuery } from '@apollo/client' import { useApolloClient, useMutation, useQuery } from '@apollo/client'
import { useDecryption, useEncryption, useSetKey, useWalletLogger, WalletStatus } from '@/wallets/client/hooks' import { useDecryption, useEncryption, useSetKey, useWalletLogger, useWalletsUpdatedAt, WalletStatus } from '@/wallets/client/hooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { import {
isEncryptedField, isTemplate, isWallet, protocolAvailable, protocolClientSchema, reverseProtocolRelationName isEncryptedField, isTemplate, isWallet, protocolAvailable, protocolClientSchema, reverseProtocolRelationName
@ -109,12 +109,13 @@ function undoFieldAlias ({ id, ...wallet }) {
function useRefetchOnChange (refetch) { function useRefetchOnChange (refetch) {
const { me } = useMe() const { me } = useMe()
const walletsUpdatedAt = useWalletsUpdatedAt()
useEffect(() => { useEffect(() => {
if (!me?.id) return if (!me?.id) return
refetch() refetch()
}, [refetch, me?.id, me?.privates?.walletsUpdatedAt]) }, [refetch, me?.id, walletsUpdatedAt])
} }
export function useWalletQuery ({ id, name }) { export function useWalletQuery ({ id, name }) {

View File

@ -1,3 +1,4 @@
import { useMe } from '@/components/me'
import { useWallets } from '@/wallets/client/context' import { useWallets } from '@/wallets/client/context'
import protocols from '@/wallets/client/protocols' import protocols from '@/wallets/client/protocols'
import { isWallet } from '@/wallets/lib/util' import { isWallet } from '@/wallets/lib/util'
@ -47,3 +48,8 @@ export function useWalletStatus (wallet) {
return useMemo(() => ({ send: wallet.send, receive: wallet.receive }), [wallet]) return useMemo(() => ({ send: wallet.send, receive: wallet.receive }), [wallet])
} }
export function useWalletsUpdatedAt () {
const { me } = useMe()
return me?.privates?.walletsUpdatedAt
}

View File

@ -142,7 +142,11 @@ async function updateWalletEncryption (parent, { keyHash, wallets }, { me, model
// make sure the user's vault key didn't change while we were updating the protocols // make sure the user's vault key didn't change while we were updating the protocols
await tx.user.update({ await tx.user.update({
where: { id: me.id, vaultKeyHash: oldKeyHash }, where: { id: me.id, vaultKeyHash: oldKeyHash },
data: { vaultKeyHash: keyHash, showPassphrase: false } data: {
vaultKeyHash: keyHash,
showPassphrase: false,
vaultKeyHashUpdatedAt: new Date()
}
}) })
return true return true
@ -154,7 +158,7 @@ async function updateKeyHash (parent, { keyHash }, { me, models }) {
const count = await models.$executeRaw` const count = await models.$executeRaw`
UPDATE users UPDATE users
SET "vaultKeyHash" = ${keyHash} SET "vaultKeyHash" = ${keyHash}, "vaultKeyHashUpdatedAt" = NOW()
WHERE id = ${me.id} WHERE id = ${me.id}
AND "vaultKeyHash" = '' AND "vaultKeyHash" = ''
` `
@ -184,7 +188,11 @@ async function resetWallets (parent, { newKeyHash }, { me, models }) {
await tx.user.update({ await tx.user.update({
where: { id: me.id, vaultKeyHash: oldHash }, where: { id: me.id, vaultKeyHash: oldHash },
// TODO(wallet-v2): nullable vaultKeyHash column // TODO(wallet-v2): nullable vaultKeyHash column
data: { vaultKeyHash: newKeyHash, showPassphrase: true } data: {
vaultKeyHash: newKeyHash,
showPassphrase: true,
vaultKeyHashUpdatedAt: new Date()
}
}) })
}) })