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!
autoWithdrawThreshold: Int
vaultKeyHash: String
vaultKeyHashUpdatedAt: Date
walletsUpdatedAt: Date
}

View File

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

View File

@ -250,3 +250,13 @@ export const truncateString = (str, maxLength, suffix = ' ...') => {
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")
oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees")
vaultKeyHash String @default("")
vaultKeyHashUpdatedAt DateTime?
showPassphrase Boolean @default(true)
walletsUpdatedAt DateTime?
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 './passphrase'
export * from './logger'
export * from './debug'

View File

@ -160,7 +160,7 @@ export function useKeyInit () {
const { key: randomKey, hash: randomHash } = await generateRandomKey()
// 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 read = tx.objectStore('vault').get('key')
@ -180,7 +180,8 @@ export function useKeyInit () {
}
// 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 = () => {
reject(write.error)
@ -188,12 +189,12 @@ export function useKeyInit () {
write.onsuccess = (event) => {
// 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) {
console.error('key init failed:', err)
}

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ import {
UPDATE_KEY_HASH
} from '@/wallets/client/fragments'
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 {
isEncryptedField, isTemplate, isWallet, protocolAvailable, protocolClientSchema, reverseProtocolRelationName
@ -109,12 +109,13 @@ function undoFieldAlias ({ id, ...wallet }) {
function useRefetchOnChange (refetch) {
const { me } = useMe()
const walletsUpdatedAt = useWalletsUpdatedAt()
useEffect(() => {
if (!me?.id) return
refetch()
}, [refetch, me?.id, me?.privates?.walletsUpdatedAt])
}, [refetch, me?.id, walletsUpdatedAt])
}
export function useWalletQuery ({ id, name }) {

View File

@ -1,3 +1,4 @@
import { useMe } from '@/components/me'
import { useWallets } from '@/wallets/client/context'
import protocols from '@/wallets/client/protocols'
import { isWallet } from '@/wallets/lib/util'
@ -47,3 +48,8 @@ export function useWalletStatus (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
await tx.user.update({
where: { id: me.id, vaultKeyHash: oldKeyHash },
data: { vaultKeyHash: keyHash, showPassphrase: false }
data: {
vaultKeyHash: keyHash,
showPassphrase: false,
vaultKeyHashUpdatedAt: new Date()
}
})
return true
@ -154,7 +158,7 @@ async function updateKeyHash (parent, { keyHash }, { me, models }) {
const count = await models.$executeRaw`
UPDATE users
SET "vaultKeyHash" = ${keyHash}
SET "vaultKeyHash" = ${keyHash}, "vaultKeyHashUpdatedAt" = NOW()
WHERE id = ${me.id}
AND "vaultKeyHash" = ''
`
@ -184,7 +188,11 @@ async function resetWallets (parent, { newKeyHash }, { me, models }) {
await tx.user.update({
where: { id: me.id, vaultKeyHash: oldHash },
// TODO(wallet-v2): nullable vaultKeyHash column
data: { vaultKeyHash: newKeyHash, showPassphrase: true }
data: {
vaultKeyHash: newKeyHash,
showPassphrase: true,
vaultKeyHashUpdatedAt: new Date()
}
})
})