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:
		
							parent
							
								
									6b440cfdf3
								
							
						
					
					
						commit
						faa26ec68f
					
				@ -202,6 +202,7 @@ export default gql`
 | 
			
		||||
    withdrawMaxFeeDefault: Int!
 | 
			
		||||
    autoWithdrawThreshold: Int
 | 
			
		||||
    vaultKeyHash: String
 | 
			
		||||
    vaultKeyHashUpdatedAt: Date
 | 
			
		||||
    walletsUpdatedAt: Date
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ ${STREAK_FIELDS}
 | 
			
		||||
      wildWestMode
 | 
			
		||||
      disableFreebies
 | 
			
		||||
      vaultKeyHash
 | 
			
		||||
      vaultKeyHashUpdatedAt
 | 
			
		||||
      walletsUpdatedAt
 | 
			
		||||
      showPassphrase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										15
									
								
								pages/wallets/debug.js
									
									
									
									
									
										Normal 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>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
-- AlterTable
 | 
			
		||||
ALTER TABLE "users" ADD COLUMN     "vaultKeyHashUpdatedAt" TIMESTAMP(3);
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										86
									
								
								wallets/client/components/debug.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								wallets/client/components/debug.js
									
									
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
@ -4,3 +4,4 @@ export * from './forms'
 | 
			
		||||
export * from './layout'
 | 
			
		||||
export * from './passphrase'
 | 
			
		||||
export * from './logger'
 | 
			
		||||
export * from './debug'
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
@ -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 }) {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user