From dd57a72e118d80a00ca9faa4fd3c8e0822abb7af Mon Sep 17 00:00:00 2001 From: TZGyn Date: Fri, 22 May 2026 03:03:03 +0800 Subject: [PATCH] fix biometric, tx dedup, loading crash; add flush cache; remove monero startup from evm init - biometric-helper: use .deviceOwnerAuthentication so passcode can unlock Touch ID when biometrics are locked out - get-transaction-history: dedup transfers by hash+category+contract instead of hash alone (was dropping token transfers sharing a tx hash) - evm-wallet.svelte.ts: add missing loading declaration (caused ReferenceError on createWallet); add flushTxCache method; comment out monero startup from evm init to speed up loading - get-history.ts: remove dead caching logic (now handled by sync engine); import getTransactionHistory from alchemy directly - backend RPC + settings UI: add flushTxCache to clear tx cache tables --- apps/desktop/src/bun/index.ts | 80 +++++-- .../lib/alchemy/get-transaction-history.ts | 7 +- .../src/bun/lib/transactions/get-history.ts | 54 +---- .../desktop/src/bun/lib/transactions/index.ts | 5 +- apps/desktop/src/bun/native/biometric-helper | Bin 54880 -> 54880 bytes .../src/bun/native/biometric-helper.swift | 4 +- apps/desktop/src/lib/electrobun.ts | 4 + .../src/lib/states/evm-wallet.svelte.ts | 212 +++++++++++++----- apps/desktop/src/routes/settings.svelte | 18 ++ 9 files changed, 262 insertions(+), 122 deletions(-) diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index c78fa01..4b9647e 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -190,6 +190,10 @@ type RPC = { params: { address: string; chainid: string } response: void } + flushTxCache: { + params: {} + response: void + } fetchTokenBalances: { params: { address: string @@ -281,12 +285,22 @@ type RPC = { } } evmCreateWallet: { - params: { name: string; phrase: string; passwordHash?: string } + params: { + name: string + phrase: string + passwordHash?: string + } response: { id: string; name: string; createdAt: string } } evmListWallets: { params: {} - response: Array<{ id: string; name: string; hasPassword: boolean; vaultKey: string; createdAt: string }> + response: Array<{ + id: string + name: string + hasPassword: boolean + vaultKey: string + createdAt: string + }> } evmGetSeed: { params: { vaultKey: string } @@ -412,10 +426,19 @@ const rpc = BrowserView.defineRPC({ try { const wallets = db.select().from(evmWallets).all() await Promise.all([ - Bun.secrets.delete({ service: 'koins', name: 'alchemy_key' }), + Bun.secrets.delete({ + service: 'koins', + name: 'alchemy_key', + }), ...wallets.flatMap((w) => [ - Bun.secrets.delete({ service: 'koins', name: w.vaultKey }), - Bun.secrets.delete({ service: 'koins', name: `evm_auth_${w.id}` }), + Bun.secrets.delete({ + service: 'koins', + name: w.vaultKey, + }), + Bun.secrets.delete({ + service: 'koins', + name: `evm_auth_${w.id}`, + }), ]), ]) sqlite.run('DELETE FROM token_metadata') @@ -431,8 +454,11 @@ const rpc = BrowserView.defineRPC({ } }, biometricCanAuth: async () => { + console.log('[biometric] checking biometric...') try { - return canPromptTouchID() + const canPrompt = canPromptTouchID() + console.log('[biometric] biometric ===', canPrompt) + return canPrompt } catch { return false } @@ -575,16 +601,27 @@ const rpc = BrowserView.defineRPC({ return combined }, fetchCachedTxHistory: async ({ address, chainid }) => { - const transfers = getCachedTransactionHistory(chainid, address) + const transfers = await getCachedTransactionHistory( + chainid, + address, + ) return transfers.map(mapTransfer) }, syncTxHistory: async ({ address, chainid }) => { - const key = await Bun.secrets.get({ service: 'koins', name: 'alchemy_key' }) + const key = await Bun.secrets.get({ + service: 'koins', + name: 'alchemy_key', + }) if (!key) return syncTransactionHistory(key, chainid, address).catch((e) => console.error('[sync] error:', e), ) }, + flushTxCache: async () => { + sqlite.run('DELETE FROM tx_history') + sqlite.run('DELETE FROM tx_sync_status') + console.log('[rpc] flushTxCache complete') + }, fetchTokenBalances: async ({ address, chainid }) => { const key = await Bun.secrets.get({ service: 'koins', @@ -799,7 +836,11 @@ const rpc = BrowserView.defineRPC({ console.log('[rpc] evmCreateWallet:', name) const id = crypto.randomUUID() const vaultKey = `evm_seed_${id}` - await Bun.secrets.set({ service: 'koins', name: vaultKey, value: phrase }) + await Bun.secrets.set({ + service: 'koins', + name: vaultKey, + value: phrase, + }) await db.insert(evmWallets).values({ id, name, @@ -823,17 +864,30 @@ const rpc = BrowserView.defineRPC({ }, evmGetSeed: async ({ vaultKey }) => { console.log('[rpc] evmGetSeed:', vaultKey) - const seed = await Bun.secrets.get({ service: 'koins', name: vaultKey }) + const seed = await Bun.secrets.get({ + service: 'koins', + name: vaultKey, + }) if (!seed) throw new Error('Seed not found in keychain') return seed }, evmDeleteWallet: async ({ id }) => { console.log('[rpc] evmDeleteWallet:', id) - const wallet = db.select().from(evmWallets).where(eq(evmWallets.id, id)).get() + const wallet = db + .select() + .from(evmWallets) + .where(eq(evmWallets.id, id)) + .get() if (!wallet) throw new Error('Wallet not found') await Promise.all([ - Bun.secrets.delete({ service: 'koins', name: wallet.vaultKey }), - Bun.secrets.delete({ service: 'koins', name: `evm_auth_${id}` }), + Bun.secrets.delete({ + service: 'koins', + name: wallet.vaultKey, + }), + Bun.secrets.delete({ + service: 'koins', + name: `evm_auth_${id}`, + }), ]) db.delete(evmWallets).where(eq(evmWallets.id, id)).run() console.log('[rpc] evmDeleteWallet complete') diff --git a/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts b/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts index 28f3cd8..1696e77 100644 --- a/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts +++ b/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts @@ -68,8 +68,9 @@ export const getTransactionHistory = async ( let lastTimestamp = '' for (const t of transfers) { if (!t.hash) continue - if (seen.has(t.hash)) continue - seen.add(t.hash) + const dedupKey = `${t.hash}_${t.category}_${t.rawContract?.address ?? ''}` + if (seen.has(dedupKey)) continue + seen.add(dedupKey) all.push(t) if (t.blockNum) { @@ -87,7 +88,7 @@ export const getTransactionHistory = async ( console.log( `[alchemy] ${direction} page ${page + 1}: ${transfers.length} transfers, ` + `block ${pageMaxBlock}, ` + - `${new Date(firstTimestamp).toISOString()} → ${new Date(lastTimestamp).toISOString()}`, + `${firstTimestamp ? new Date(firstTimestamp).toISOString() : '?'} → ${lastTimestamp ? new Date(lastTimestamp).toISOString() : '?'}`, ) if (pageMaxBlock === 0) break diff --git a/apps/desktop/src/bun/lib/transactions/get-history.ts b/apps/desktop/src/bun/lib/transactions/get-history.ts index b79c11e..a7a3b57 100644 --- a/apps/desktop/src/bun/lib/transactions/get-history.ts +++ b/apps/desktop/src/bun/lib/transactions/get-history.ts @@ -1,7 +1,6 @@ import { eq, and, or, desc } from 'drizzle-orm' import { db } from '../db' import { txHistory } from '../db/schema' -import { getTransactionHistory as fetchFromAlchemy } from '../alchemy/get-transaction-history' import type { Transaction } from '../alchemy/types' export const getTransactionHistory = async ( @@ -10,58 +9,7 @@ export const getTransactionHistory = async ( address: string, opts: { maxCount: number }, ) => { - const result = await fetchFromAlchemy(key, chainid, address, opts) - - if (result) { - for (const t of result) { - if (!t.hash) continue - const existing = await db.query.txHistory.findFirst({ - where: { - hash: t.hash, - chainId: chainid, - }, - }) - - if (!existing) { - await db.insert(txHistory).values({ - chainId: chainid, - hash: t.hash, - blockNum: t.blockNum - ? String(parseInt(t.blockNum, 16)) - : null, - from: t.from?.toLowerCase() ?? null, - to: t.to?.toLowerCase() ?? null, - value: t.value?.toString() ?? null, - asset: t.asset ?? null, - category: t.category ?? null, - rawContractAddress: - t.rawContract?.address?.toLowerCase() ?? null, - rawContractDecimal: t.rawContract?.decimal ?? null, - metadataBlockTimestamp: t.metadata?.blockTimestamp ?? null, - raw: JSON.stringify(t), - syncedAt: new Date().toISOString(), - }) - } - } - } - - const addrLower = address.toLowerCase() - const rows = db - .select() - .from(txHistory) - .where( - and( - eq(txHistory.chainId, chainid), - or( - eq(txHistory.from, addrLower), - eq(txHistory.to, addrLower), - ), - ), - ) - .orderBy(desc(txHistory.blockNum)) - .all() - - return rows.map(toTransfer) + return getCachedTransactionHistory(chainid, address) } export const getCachedTransactionHistory = ( diff --git a/apps/desktop/src/bun/lib/transactions/index.ts b/apps/desktop/src/bun/lib/transactions/index.ts index 5066474..422254f 100644 --- a/apps/desktop/src/bun/lib/transactions/index.ts +++ b/apps/desktop/src/bun/lib/transactions/index.ts @@ -1,2 +1,5 @@ -export { getTransactionHistory, getCachedTransactionHistory } from './get-history' +export { + getTransactionHistory, + getCachedTransactionHistory, +} from './get-history' export { getTransactionDetails } from './get-details' diff --git a/apps/desktop/src/bun/native/biometric-helper b/apps/desktop/src/bun/native/biometric-helper index 98b81668dfeb8de2a8b850f055f328f2e38a0713..d6875f9ba759315c9c349342807009e480d9f300 100755 GIT binary patch delta 768 zcmXw1Ye-XJ7=FL=>71!^PR)5KX8x#wM$*mOax=*x3oosk8H#h5U=+GxAi;?YA`Cno z|MWvrE83YR>IpQWG-D)Tk>ZsSSId$l`jOe0H0+(O1K;`H=X;;`_I#hahq`-c>l|t0 z<_n_CN&o@?<^CUjW-*-%1}ppLR#mDW*;nr+D#pdzI~GMrHb&FeYn};G1rgRxGs0>S z6?{!Wf{zkzgOZ6o0Il*^y;n~3UdQ!B34om}=YMa@uCNhkdxC3~g7Yl0G;!cPh5O7X zaElChJ3VB=O}9ID1+k?RqZ*Mr%jms>R4v`74Ekc6dBB$PfgBz5j5LCA)rdPhgy+L_ zu?YYwKkq!A88LUl5@a6Ym%<(tC+BdEjVxiay-%#gU0_wyH#G2%V}}f zb_Q{dN0|a9onSE0A8h!4aSt~5g~mDa`T6in%Qs~yz;_TUus3<+mVY4Np9)}4!0#{Oe=TbPY!drj zlZvvo%_8Wks=aj4YOAlcq+PJqUAERk?@F>d&tCANJSC%fFC2ek?uEP)RdLhp*~`Y! zq6S0GcU_W3+5MsJpzQ0Xw>^!Dxx^!D?n%qt_WfNt`^}g9!jxV!X-i#}9xC3CKctwXCZ;Vvh!g}w7 zsEe7*RF*0L5CD`4hY^z$)JCLrXJYKxl znJBWFFrjY}VuGUmLpauV3aqAHgTO~S6G_exZViL82N(KW6!@zN?om^0yG$1o2M{UT zDRHi;3L*20T=*{KG1{rZIRq2xqgn(kf?bgVjR!X zIMUQZkwzOt?7#RNF*%Q8o$0J6Op8ibnOv1+K%PMBc`|ke8Q(zGkWSX2=eEqB94=fj zPLq~k1!5Zp4pmB-VgNgmSWUBJmQ|9klJ;37SFsK9lN62Fr}AtR%CE@Xn0G0Q5GrN& zM1w6C+udu)UV3o2*!a3D(_KH2ODA{athzVZvD{zw zuVw9R { let transactions = $state([]) let loadingBalance = $state(false) let loadingTransactions = $state(false) + let loading = $state(false) let error = $state('') let wallets = $state([]) let currentWalletId = $state(null) @@ -62,12 +67,36 @@ export const EvmWallet = () => { const net = () => networks.find((n) => n.id === network)! - async function hashPassword(password: string, salt?: string): Promise<{ salt: string; hash: string }> { - const s = salt ?? Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('') + async function hashPassword( + password: string, + salt?: string, + ): Promise<{ salt: string; hash: string }> { + const s = + salt ?? + Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => + b.toString(16).padStart(2, '0'), + ).join('') const enc = new TextEncoder() - const key = await crypto.subtle.importKey('raw', enc.encode(password + s), 'PBKDF2', false, ['deriveBits']) - const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: enc.encode(s), iterations: 100_000, hash: 'SHA-256' }, key, 256) - const hash = Array.from(new Uint8Array(bits), b => b.toString(16).padStart(2, '0')).join('') + const key = await crypto.subtle.importKey( + 'raw', + enc.encode(password + s), + 'PBKDF2', + false, + ['deriveBits'], + ) + const bits = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt: enc.encode(s), + iterations: 100_000, + hash: 'SHA-256', + }, + key, + 256, + ) + const hash = Array.from(new Uint8Array(bits), (b) => + b.toString(16).padStart(2, '0'), + ).join('') return { salt: s, hash } } @@ -93,15 +122,19 @@ export const EvmWallet = () => { } const [key] = await tryCatch( - electrobun.rpc.request.getSecret({ service: 'koins', name: 'alchemy_key' }), + electrobun.rpc.request.getSecret({ + service: 'koins', + name: 'alchemy_key', + }), ) if (key) apiKey = key - await moneroWallet.checkStatus() - if (moneroWallet.installed && !moneroWallet.running && !moneroWallet.downloading) { - await moneroWallet.start() - await moneroWallet.checkStatus() - } + // await moneroWallet.checkStatus() + // if (moneroWallet.installed && !moneroWallet.running && !moneroWallet.downloading) { + // await moneroWallet.start() + // await moneroWallet.checkStatus() + // } + await checkBiometric() ready = true } @@ -141,7 +174,9 @@ export const EvmWallet = () => { if (!wallet) return false currentWalletId = walletId const [vaultSeed] = await tryCatch( - electrobun.rpc.request.evmGetSeed({ vaultKey: wallet.vaultKey }), + electrobun.rpc.request.evmGetSeed({ + vaultKey: wallet.vaultKey, + }), ) if (!vaultSeed) return false seed = vaultSeed @@ -152,7 +187,9 @@ export const EvmWallet = () => { const unlockWithBiometrics = async () => { if (!electrobun.rpc) return false const [authed] = await tryCatch( - electrobun.rpc.request.biometricAuth({ reason: 'Unlock wallet' }), + electrobun.rpc.request.biometricAuth({ + reason: 'Unlock wallet', + }), ) if (!authed) return false if (!currentWalletId) return false @@ -177,7 +214,9 @@ export const EvmWallet = () => { const wallet = wallets.find((w) => w.id === walletId) if (!wallet) return false const [vaultSeed] = await tryCatch( - electrobun.rpc.request.evmGetSeed({ vaultKey: wallet.vaultKey }), + electrobun.rpc.request.evmGetSeed({ + vaultKey: wallet.vaultKey, + }), ) if (!vaultSeed) return false seed = vaultSeed @@ -213,7 +252,11 @@ export const EvmWallet = () => { return false } - const createWallet = async (name: string, phrase: string, password?: string) => { + const createWallet = async ( + name: string, + phrase: string, + password?: string, + ) => { loading = true error = '' try { @@ -288,7 +331,6 @@ export const EvmWallet = () => { const client = createPublicClient({ transport: http(net().rpc) }) syncTxHistory() - ;(async () => { const [nativeBal] = await tryCatch( client.getBalance({ address: account.address }), @@ -296,13 +338,13 @@ export const EvmWallet = () => { if (nativeBal) balance = formatEther(nativeBal) const [bals] = await tryCatch( electrobun.rpc.request.fetchTokenBalances({ - address, chainid: net().chainid, + address, + chainid: net().chainid, }), ) if (bals) tokenBalances = bals loadingBalance = false })() - ;(async () => { await fetchTxHistory() loadingTransactions = false @@ -311,12 +353,22 @@ export const EvmWallet = () => { const syncTxHistory = () => { if (!address || !apiKey || !electrobun.rpc) return - electrobun.rpc.request.syncTxHistory({ address, chainid: net().chainid }) + electrobun.rpc.request.syncTxHistory({ + address, + chainid: net().chainid, + }) + } + + const flushTxCache = async () => { + await electrobun.rpc?.request.flushTxCache({}) + await fetchTxHistory() } const saveApiKey = async (key: string) => { await electrobun.rpc?.request.setSecret({ - service: 'koins', name: 'alchemy_key', value: key, + service: 'koins', + name: 'alchemy_key', + value: key, }) apiKey = key syncTxHistory() @@ -329,7 +381,8 @@ export const EvmWallet = () => { return } const txs = await electrobun.rpc?.request.fetchCachedTxHistory({ - address, chainid: net().chainid, + address, + chainid: net().chainid, }) transactions = txs ?? [] } @@ -354,34 +407,92 @@ export const EvmWallet = () => { } return { - get accountType() { return accountType }, - get ready() { return ready }, - get biometricAvailable() { return biometricAvailable }, - get seed() { return seed }, - get address() { return address }, - get balance() { return balance }, - get tokenBalances() { return tokenBalances }, - get network() { return network }, - get chainid() { return net().chainid }, - get networkName() { return net().name }, - get symbol() { return net().symbol }, - get explorerUrl() { return net().explorerUrl }, - get explorerAddressUrl() { return address ? `${net().explorerUrl.replace('/tx/', '/address/')}${address}` : '' }, - get networks() { return networks }, - get apiKey() { return apiKey }, - get transactions() { return transactions }, - get loading() { return loadingBalance || loadingTransactions }, - get loadingBalance() { return loadingBalance }, - get loadingTransactions() { return loadingTransactions }, - get error() { return error }, - get wallets() { return wallets }, - get currentWalletId() { return currentWalletId }, - get currentWallet() { return wallets.find((w) => w.id === currentWalletId) ?? null }, - get isLocked() { return !!currentWalletId && !seed }, - get hasWallets() { return wallets.length > 0 }, - get currentPasswordHash() { return currentPasswordHash }, - set apiKey(v: string) { apiKey = v }, - set error(v: string) { error = v }, + get accountType() { + return accountType + }, + get ready() { + return ready + }, + get biometricAvailable() { + return biometricAvailable + }, + get seed() { + return seed + }, + get address() { + return address + }, + get balance() { + return balance + }, + get tokenBalances() { + return tokenBalances + }, + get network() { + return network + }, + get chainid() { + return net().chainid + }, + get networkName() { + return net().name + }, + get symbol() { + return net().symbol + }, + get explorerUrl() { + return net().explorerUrl + }, + get explorerAddressUrl() { + return address + ? `${net().explorerUrl.replace('/tx/', '/address/')}${address}` + : '' + }, + get networks() { + return networks + }, + get apiKey() { + return apiKey + }, + get transactions() { + return transactions + }, + get loading() { + return loading || loadingBalance || loadingTransactions + }, + get loadingBalance() { + return loadingBalance + }, + get loadingTransactions() { + return loadingTransactions + }, + get error() { + return error + }, + get wallets() { + return wallets + }, + get currentWalletId() { + return currentWalletId + }, + get currentWallet() { + return wallets.find((w) => w.id === currentWalletId) ?? null + }, + get isLocked() { + return !!currentWalletId && !seed + }, + get hasWallets() { + return wallets.length > 0 + }, + get currentPasswordHash() { + return currentPasswordHash + }, + set apiKey(v: string) { + apiKey = v + }, + set error(v: string) { + error = v + }, init, login, logout, @@ -401,6 +512,7 @@ export const EvmWallet = () => { deleteWallet, clearSelection, syncTxHistory, + flushTxCache, } } diff --git a/apps/desktop/src/routes/settings.svelte b/apps/desktop/src/routes/settings.svelte index c9cdfa9..aa9e6f9 100644 --- a/apps/desktop/src/routes/settings.svelte +++ b/apps/desktop/src/routes/settings.svelte @@ -17,6 +17,7 @@ let confirm = $state(false) let resetting = $state(false) let deletingWalletId = $state(null) + let flushing = $state(false) async function handleReset() { resetting = true @@ -31,6 +32,12 @@ deletingWalletId = null if (w.wallets.length === 0) navigate('/multicoin') } + + async function handleFlushCache() { + flushing = true + await w.flushTxCache() + flushing = false + }
@@ -84,6 +91,17 @@ App preferences and data management +
+

Transaction Cache

+

+ Clears all cached transaction history and sync status. Data will be re-fetched from Alchemy on next refresh. +

+
+ +
+

Reset App