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
This commit is contained in:
2026-05-22 03:03:03 +08:00
parent edd4f6fff0
commit dd57a72e11
9 changed files with 262 additions and 122 deletions
+67 -13
View File
@@ -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<RPC>({
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<RPC>({
}
},
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<RPC>({
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<RPC>({
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<RPC>({
},
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')
@@ -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
@@ -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 = (
@@ -1,2 +1,5 @@
export { getTransactionHistory, getCachedTransactionHistory } from './get-history'
export {
getTransactionHistory,
getCachedTransactionHistory,
} from './get-history'
export { getTransactionDetails } from './get-details'
Binary file not shown.
@@ -3,7 +3,7 @@ import LocalAuthentication
let context = LAContext()
var error: NSError?
let available = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
let available = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
let isCheck = CommandLine.arguments.contains("--check")
if isCheck {
@@ -19,7 +19,7 @@ guard available else {
let reason = CommandLine.arguments.last ?? "Authenticate"
let sem = DispatchSemaphore(value: 0)
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authError in
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
if success {
exit(0)
}