diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 8c768b3a..8414c0b9 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -202,6 +202,7 @@ export default gql`
withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int
vaultKeyHash: String
+ vaultKeyHashUpdatedAt: Date
walletsUpdatedAt: Date
}
diff --git a/fragments/users.js b/fragments/users.js
index 66107639..5e81c585 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -48,6 +48,7 @@ ${STREAK_FIELDS}
wildWestMode
disableFreebies
vaultKeyHash
+ vaultKeyHashUpdatedAt
walletsUpdatedAt
showPassphrase
}
diff --git a/lib/format.js b/lib/format.js
index 8a59ffb7..49159a77 100644
--- a/lib/format.js
+++ b/lib/format.js
@@ -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]}`
+}
diff --git a/pages/wallets/debug.js b/pages/wallets/debug.js
new file mode 100644
index 00000000..38d31019
--- /dev/null
+++ b/pages/wallets/debug.js
@@ -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 (
+
+
+ wallet debug
+
+
+
+ )
+}
diff --git a/prisma/migrations/20250721181629_vault_key_hash_updated_at/migration.sql b/prisma/migrations/20250721181629_vault_key_hash_updated_at/migration.sql
new file mode 100644
index 00000000..dbc19f59
--- /dev/null
+++ b/prisma/migrations/20250721181629_vault_key_hash_updated_at/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "vaultKeyHashUpdatedAt" TIMESTAMP(3);
+
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e6a0ca17..1c834f63 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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)
diff --git a/wallets/client/components/debug.js b/wallets/client/components/debug.js
new file mode 100644
index 00000000..b4b62a4b
--- /dev/null
+++ b/wallets/client/components/debug.js
@@ -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 (
+
+
+
persistent storage:
+
{persistent !== null ? persistent?.toString() : 'unknown'}
+
+
+
storage quota:
+
{quota !== null ? formatBytes(quota) : 'unknown'}
+
+
+
storage usage:
+
{usage !== null ? formatBytes(usage) : 'unknown'}
+
+
+
storage remaining:
+
{usage !== null && quota !== null ? formatBytes(quota - usage) : 'unknown'}
+
+
+
device key hash:
+
{localKeyHash ? shortHash(localKeyHash) : 'unknown'}
+
+
+
server key hash:
+
{remoteKeyHash ? shortHash(remoteKeyHash) : 'unknown'}
+
+
+
last device key update:
+
+ {localKeyUpdatedAt ? timeSince(localKeyUpdatedAt) : 'unknown'}
+
+
+
+
last server key update:
+
+ {remoteKeyHashUpdatedAt ? timeSince(new Date(remoteKeyHashUpdatedAt).getTime()) : 'unknown'}
+
+
+
+
last wallet update:
+
+ {walletsUpdatedAt ? timeSince(new Date(walletsUpdatedAt).getTime()) : 'unknown'}
+
+
+
+ )
+}
+
+function shortHash (hash) {
+ return hash.slice(0, 6) + '...' + hash.slice(-6)
+}
diff --git a/wallets/client/components/index.js b/wallets/client/components/index.js
index 76755a07..6c35ddef 100644
--- a/wallets/client/components/index.js
+++ b/wallets/client/components/index.js
@@ -4,3 +4,4 @@ export * from './forms'
export * from './layout'
export * from './passphrase'
export * from './logger'
+export * from './debug'
diff --git a/wallets/client/context/hooks.js b/wallets/client/context/hooks.js
index e64e3832..107cb9a5 100644
--- a/wallets/client/context/hooks.js
+++ b/wallets/client/context/hooks.js
@@ -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)
}
diff --git a/wallets/client/context/provider.js b/wallets/client/context/provider.js
index 3613bbdd..a86f2104 100644
--- a/wallets/client/context/provider.js
+++ b/wallets/client/context/provider.js
@@ -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
})
diff --git a/wallets/client/context/reducer.js b/wallets/client/context/reducer.js
index e1018737..688f82d6 100644
--- a/wallets/client/context/reducer.js
+++ b/wallets/client/context/reducer.js
@@ -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 {
diff --git a/wallets/client/hooks/crypto.js b/wallets/client/hooks/crypto.js
index fbb2d6c1..c077eba8 100644
--- a/wallets/client/hooks/crypto.js
+++ b/wallets/client/hooks/crypto.js
@@ -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()
diff --git a/wallets/client/hooks/query.js b/wallets/client/hooks/query.js
index 8edf6b93..23288e81 100644
--- a/wallets/client/hooks/query.js
+++ b/wallets/client/hooks/query.js
@@ -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 }) {
diff --git a/wallets/client/hooks/wallet.js b/wallets/client/hooks/wallet.js
index 95c14c41..8a439825 100644
--- a/wallets/client/hooks/wallet.js
+++ b/wallets/client/hooks/wallet.js
@@ -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
+}
diff --git a/wallets/server/resolvers/wallet.js b/wallets/server/resolvers/wallet.js
index 54f4bdf7..d4816891 100644
--- a/wallets/server/resolvers/wallet.js
+++ b/wallets/server/resolvers/wallet.js
@@ -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()
+ }
})
})