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:
@@ -190,6 +190,10 @@ type RPC = {
|
|||||||
params: { address: string; chainid: string }
|
params: { address: string; chainid: string }
|
||||||
response: void
|
response: void
|
||||||
}
|
}
|
||||||
|
flushTxCache: {
|
||||||
|
params: {}
|
||||||
|
response: void
|
||||||
|
}
|
||||||
fetchTokenBalances: {
|
fetchTokenBalances: {
|
||||||
params: {
|
params: {
|
||||||
address: string
|
address: string
|
||||||
@@ -281,12 +285,22 @@ type RPC = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
evmCreateWallet: {
|
evmCreateWallet: {
|
||||||
params: { name: string; phrase: string; passwordHash?: string }
|
params: {
|
||||||
|
name: string
|
||||||
|
phrase: string
|
||||||
|
passwordHash?: string
|
||||||
|
}
|
||||||
response: { id: string; name: string; createdAt: string }
|
response: { id: string; name: string; createdAt: string }
|
||||||
}
|
}
|
||||||
evmListWallets: {
|
evmListWallets: {
|
||||||
params: {}
|
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: {
|
evmGetSeed: {
|
||||||
params: { vaultKey: string }
|
params: { vaultKey: string }
|
||||||
@@ -412,10 +426,19 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
try {
|
try {
|
||||||
const wallets = db.select().from(evmWallets).all()
|
const wallets = db.select().from(evmWallets).all()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Bun.secrets.delete({ service: 'koins', name: 'alchemy_key' }),
|
Bun.secrets.delete({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
}),
|
||||||
...wallets.flatMap((w) => [
|
...wallets.flatMap((w) => [
|
||||||
Bun.secrets.delete({ service: 'koins', name: w.vaultKey }),
|
Bun.secrets.delete({
|
||||||
Bun.secrets.delete({ service: 'koins', name: `evm_auth_${w.id}` }),
|
service: 'koins',
|
||||||
|
name: w.vaultKey,
|
||||||
|
}),
|
||||||
|
Bun.secrets.delete({
|
||||||
|
service: 'koins',
|
||||||
|
name: `evm_auth_${w.id}`,
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
sqlite.run('DELETE FROM token_metadata')
|
sqlite.run('DELETE FROM token_metadata')
|
||||||
@@ -431,8 +454,11 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
biometricCanAuth: async () => {
|
biometricCanAuth: async () => {
|
||||||
|
console.log('[biometric] checking biometric...')
|
||||||
try {
|
try {
|
||||||
return canPromptTouchID()
|
const canPrompt = canPromptTouchID()
|
||||||
|
console.log('[biometric] biometric ===', canPrompt)
|
||||||
|
return canPrompt
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -575,16 +601,27 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
return combined
|
return combined
|
||||||
},
|
},
|
||||||
fetchCachedTxHistory: async ({ address, chainid }) => {
|
fetchCachedTxHistory: async ({ address, chainid }) => {
|
||||||
const transfers = getCachedTransactionHistory(chainid, address)
|
const transfers = await getCachedTransactionHistory(
|
||||||
|
chainid,
|
||||||
|
address,
|
||||||
|
)
|
||||||
return transfers.map(mapTransfer)
|
return transfers.map(mapTransfer)
|
||||||
},
|
},
|
||||||
syncTxHistory: async ({ address, chainid }) => {
|
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
|
if (!key) return
|
||||||
syncTransactionHistory(key, chainid, address).catch((e) =>
|
syncTransactionHistory(key, chainid, address).catch((e) =>
|
||||||
console.error('[sync] error:', 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 }) => {
|
fetchTokenBalances: async ({ address, chainid }) => {
|
||||||
const key = await Bun.secrets.get({
|
const key = await Bun.secrets.get({
|
||||||
service: 'koins',
|
service: 'koins',
|
||||||
@@ -799,7 +836,11 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
console.log('[rpc] evmCreateWallet:', name)
|
console.log('[rpc] evmCreateWallet:', name)
|
||||||
const id = crypto.randomUUID()
|
const id = crypto.randomUUID()
|
||||||
const vaultKey = `evm_seed_${id}`
|
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({
|
await db.insert(evmWallets).values({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -823,17 +864,30 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
},
|
},
|
||||||
evmGetSeed: async ({ vaultKey }) => {
|
evmGetSeed: async ({ vaultKey }) => {
|
||||||
console.log('[rpc] evmGetSeed:', 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')
|
if (!seed) throw new Error('Seed not found in keychain')
|
||||||
return seed
|
return seed
|
||||||
},
|
},
|
||||||
evmDeleteWallet: async ({ id }) => {
|
evmDeleteWallet: async ({ id }) => {
|
||||||
console.log('[rpc] evmDeleteWallet:', 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')
|
if (!wallet) throw new Error('Wallet not found')
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Bun.secrets.delete({ service: 'koins', name: wallet.vaultKey }),
|
Bun.secrets.delete({
|
||||||
Bun.secrets.delete({ service: 'koins', name: `evm_auth_${id}` }),
|
service: 'koins',
|
||||||
|
name: wallet.vaultKey,
|
||||||
|
}),
|
||||||
|
Bun.secrets.delete({
|
||||||
|
service: 'koins',
|
||||||
|
name: `evm_auth_${id}`,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
db.delete(evmWallets).where(eq(evmWallets.id, id)).run()
|
db.delete(evmWallets).where(eq(evmWallets.id, id)).run()
|
||||||
console.log('[rpc] evmDeleteWallet complete')
|
console.log('[rpc] evmDeleteWallet complete')
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ export const getTransactionHistory = async (
|
|||||||
let lastTimestamp = ''
|
let lastTimestamp = ''
|
||||||
for (const t of transfers) {
|
for (const t of transfers) {
|
||||||
if (!t.hash) continue
|
if (!t.hash) continue
|
||||||
if (seen.has(t.hash)) continue
|
const dedupKey = `${t.hash}_${t.category}_${t.rawContract?.address ?? ''}`
|
||||||
seen.add(t.hash)
|
if (seen.has(dedupKey)) continue
|
||||||
|
seen.add(dedupKey)
|
||||||
all.push(t)
|
all.push(t)
|
||||||
|
|
||||||
if (t.blockNum) {
|
if (t.blockNum) {
|
||||||
@@ -87,7 +88,7 @@ export const getTransactionHistory = async (
|
|||||||
console.log(
|
console.log(
|
||||||
`[alchemy] ${direction} page ${page + 1}: ${transfers.length} transfers, ` +
|
`[alchemy] ${direction} page ${page + 1}: ${transfers.length} transfers, ` +
|
||||||
`block ${pageMaxBlock}, ` +
|
`block ${pageMaxBlock}, ` +
|
||||||
`${new Date(firstTimestamp).toISOString()} → ${new Date(lastTimestamp).toISOString()}`,
|
`${firstTimestamp ? new Date(firstTimestamp).toISOString() : '?'} → ${lastTimestamp ? new Date(lastTimestamp).toISOString() : '?'}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (pageMaxBlock === 0) break
|
if (pageMaxBlock === 0) break
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { eq, and, or, desc } from 'drizzle-orm'
|
import { eq, and, or, desc } from 'drizzle-orm'
|
||||||
import { db } from '../db'
|
import { db } from '../db'
|
||||||
import { txHistory } from '../db/schema'
|
import { txHistory } from '../db/schema'
|
||||||
import { getTransactionHistory as fetchFromAlchemy } from '../alchemy/get-transaction-history'
|
|
||||||
import type { Transaction } from '../alchemy/types'
|
import type { Transaction } from '../alchemy/types'
|
||||||
|
|
||||||
export const getTransactionHistory = async (
|
export const getTransactionHistory = async (
|
||||||
@@ -10,58 +9,7 @@ export const getTransactionHistory = async (
|
|||||||
address: string,
|
address: string,
|
||||||
opts: { maxCount: number },
|
opts: { maxCount: number },
|
||||||
) => {
|
) => {
|
||||||
const result = await fetchFromAlchemy(key, chainid, address, opts)
|
return getCachedTransactionHistory(chainid, address)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCachedTransactionHistory = (
|
export const getCachedTransactionHistory = (
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
export { getTransactionHistory, getCachedTransactionHistory } from './get-history'
|
export {
|
||||||
|
getTransactionHistory,
|
||||||
|
getCachedTransactionHistory,
|
||||||
|
} from './get-history'
|
||||||
export { getTransactionDetails } from './get-details'
|
export { getTransactionDetails } from './get-details'
|
||||||
|
|||||||
Binary file not shown.
@@ -3,7 +3,7 @@ import LocalAuthentication
|
|||||||
|
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
var error: NSError?
|
var error: NSError?
|
||||||
let available = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
|
let available = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
|
||||||
|
|
||||||
let isCheck = CommandLine.arguments.contains("--check")
|
let isCheck = CommandLine.arguments.contains("--check")
|
||||||
if isCheck {
|
if isCheck {
|
||||||
@@ -19,7 +19,7 @@ guard available else {
|
|||||||
let reason = CommandLine.arguments.last ?? "Authenticate"
|
let reason = CommandLine.arguments.last ?? "Authenticate"
|
||||||
let sem = DispatchSemaphore(value: 0)
|
let sem = DispatchSemaphore(value: 0)
|
||||||
|
|
||||||
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authError in
|
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
|
||||||
if success {
|
if success {
|
||||||
exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ type RPC = {
|
|||||||
params: { address: string; chainid: string }
|
params: { address: string; chainid: string }
|
||||||
response: void
|
response: void
|
||||||
}
|
}
|
||||||
|
flushTxCache: {
|
||||||
|
params: {}
|
||||||
|
response: void
|
||||||
|
}
|
||||||
fetchTokenBalances: {
|
fetchTokenBalances: {
|
||||||
params: {
|
params: {
|
||||||
address: string
|
address: string
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { mnemonicToAccount } from 'viem/accounts'
|
import { mnemonicToAccount } from 'viem/accounts'
|
||||||
import { createPublicClient, http, formatEther } from 'viem'
|
import { createPublicClient, http, formatEther } from 'viem'
|
||||||
import { electrobun, type TxEntry, type EvmWalletInfo } from '$lib/electrobun'
|
import {
|
||||||
|
electrobun,
|
||||||
|
type TxEntry,
|
||||||
|
type EvmWalletInfo,
|
||||||
|
} from '$lib/electrobun'
|
||||||
import { tryCatch } from '@koins/utils'
|
import { tryCatch } from '@koins/utils'
|
||||||
import { moneroWallet } from './monero-wallet.svelte.js'
|
import { moneroWallet } from './monero-wallet.svelte.js'
|
||||||
|
|
||||||
@@ -55,6 +59,7 @@ export const EvmWallet = () => {
|
|||||||
let transactions = $state<TxEntry[]>([])
|
let transactions = $state<TxEntry[]>([])
|
||||||
let loadingBalance = $state(false)
|
let loadingBalance = $state(false)
|
||||||
let loadingTransactions = $state(false)
|
let loadingTransactions = $state(false)
|
||||||
|
let loading = $state(false)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
let wallets = $state<EvmWalletInfo[]>([])
|
let wallets = $state<EvmWalletInfo[]>([])
|
||||||
let currentWalletId = $state<string | null>(null)
|
let currentWalletId = $state<string | null>(null)
|
||||||
@@ -62,12 +67,36 @@ export const EvmWallet = () => {
|
|||||||
|
|
||||||
const net = () => networks.find((n) => n.id === network)!
|
const net = () => networks.find((n) => n.id === network)!
|
||||||
|
|
||||||
async function hashPassword(password: string, salt?: string): Promise<{ salt: string; hash: string }> {
|
async function hashPassword(
|
||||||
const s = salt ?? Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('')
|
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 enc = new TextEncoder()
|
||||||
const key = await crypto.subtle.importKey('raw', enc.encode(password + s), 'PBKDF2', false, ['deriveBits'])
|
const key = await crypto.subtle.importKey(
|
||||||
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: enc.encode(s), iterations: 100_000, hash: 'SHA-256' }, key, 256)
|
'raw',
|
||||||
const hash = Array.from(new Uint8Array(bits), b => b.toString(16).padStart(2, '0')).join('')
|
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 }
|
return { salt: s, hash }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,15 +122,19 @@ export const EvmWallet = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [key] = await tryCatch(
|
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
|
if (key) apiKey = key
|
||||||
|
|
||||||
await moneroWallet.checkStatus()
|
// await moneroWallet.checkStatus()
|
||||||
if (moneroWallet.installed && !moneroWallet.running && !moneroWallet.downloading) {
|
// if (moneroWallet.installed && !moneroWallet.running && !moneroWallet.downloading) {
|
||||||
await moneroWallet.start()
|
// await moneroWallet.start()
|
||||||
await moneroWallet.checkStatus()
|
// await moneroWallet.checkStatus()
|
||||||
}
|
// }
|
||||||
|
|
||||||
await checkBiometric()
|
await checkBiometric()
|
||||||
ready = true
|
ready = true
|
||||||
}
|
}
|
||||||
@@ -141,7 +174,9 @@ export const EvmWallet = () => {
|
|||||||
if (!wallet) return false
|
if (!wallet) return false
|
||||||
currentWalletId = walletId
|
currentWalletId = walletId
|
||||||
const [vaultSeed] = await tryCatch(
|
const [vaultSeed] = await tryCatch(
|
||||||
electrobun.rpc.request.evmGetSeed({ vaultKey: wallet.vaultKey }),
|
electrobun.rpc.request.evmGetSeed({
|
||||||
|
vaultKey: wallet.vaultKey,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
if (!vaultSeed) return false
|
if (!vaultSeed) return false
|
||||||
seed = vaultSeed
|
seed = vaultSeed
|
||||||
@@ -152,7 +187,9 @@ export const EvmWallet = () => {
|
|||||||
const unlockWithBiometrics = async () => {
|
const unlockWithBiometrics = async () => {
|
||||||
if (!electrobun.rpc) return false
|
if (!electrobun.rpc) return false
|
||||||
const [authed] = await tryCatch(
|
const [authed] = await tryCatch(
|
||||||
electrobun.rpc.request.biometricAuth({ reason: 'Unlock wallet' }),
|
electrobun.rpc.request.biometricAuth({
|
||||||
|
reason: 'Unlock wallet',
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
if (!authed) return false
|
if (!authed) return false
|
||||||
if (!currentWalletId) return false
|
if (!currentWalletId) return false
|
||||||
@@ -177,7 +214,9 @@ export const EvmWallet = () => {
|
|||||||
const wallet = wallets.find((w) => w.id === walletId)
|
const wallet = wallets.find((w) => w.id === walletId)
|
||||||
if (!wallet) return false
|
if (!wallet) return false
|
||||||
const [vaultSeed] = await tryCatch(
|
const [vaultSeed] = await tryCatch(
|
||||||
electrobun.rpc.request.evmGetSeed({ vaultKey: wallet.vaultKey }),
|
electrobun.rpc.request.evmGetSeed({
|
||||||
|
vaultKey: wallet.vaultKey,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
if (!vaultSeed) return false
|
if (!vaultSeed) return false
|
||||||
seed = vaultSeed
|
seed = vaultSeed
|
||||||
@@ -213,7 +252,11 @@ export const EvmWallet = () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const createWallet = async (name: string, phrase: string, password?: string) => {
|
const createWallet = async (
|
||||||
|
name: string,
|
||||||
|
phrase: string,
|
||||||
|
password?: string,
|
||||||
|
) => {
|
||||||
loading = true
|
loading = true
|
||||||
error = ''
|
error = ''
|
||||||
try {
|
try {
|
||||||
@@ -288,7 +331,6 @@ export const EvmWallet = () => {
|
|||||||
const client = createPublicClient({ transport: http(net().rpc) })
|
const client = createPublicClient({ transport: http(net().rpc) })
|
||||||
|
|
||||||
syncTxHistory()
|
syncTxHistory()
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const [nativeBal] = await tryCatch(
|
const [nativeBal] = await tryCatch(
|
||||||
client.getBalance({ address: account.address }),
|
client.getBalance({ address: account.address }),
|
||||||
@@ -296,13 +338,13 @@ export const EvmWallet = () => {
|
|||||||
if (nativeBal) balance = formatEther(nativeBal)
|
if (nativeBal) balance = formatEther(nativeBal)
|
||||||
const [bals] = await tryCatch(
|
const [bals] = await tryCatch(
|
||||||
electrobun.rpc.request.fetchTokenBalances({
|
electrobun.rpc.request.fetchTokenBalances({
|
||||||
address, chainid: net().chainid,
|
address,
|
||||||
|
chainid: net().chainid,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (bals) tokenBalances = bals
|
if (bals) tokenBalances = bals
|
||||||
loadingBalance = false
|
loadingBalance = false
|
||||||
})()
|
})()
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
await fetchTxHistory()
|
await fetchTxHistory()
|
||||||
loadingTransactions = false
|
loadingTransactions = false
|
||||||
@@ -311,12 +353,22 @@ export const EvmWallet = () => {
|
|||||||
|
|
||||||
const syncTxHistory = () => {
|
const syncTxHistory = () => {
|
||||||
if (!address || !apiKey || !electrobun.rpc) return
|
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) => {
|
const saveApiKey = async (key: string) => {
|
||||||
await electrobun.rpc?.request.setSecret({
|
await electrobun.rpc?.request.setSecret({
|
||||||
service: 'koins', name: 'alchemy_key', value: key,
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
value: key,
|
||||||
})
|
})
|
||||||
apiKey = key
|
apiKey = key
|
||||||
syncTxHistory()
|
syncTxHistory()
|
||||||
@@ -329,7 +381,8 @@ export const EvmWallet = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const txs = await electrobun.rpc?.request.fetchCachedTxHistory({
|
const txs = await electrobun.rpc?.request.fetchCachedTxHistory({
|
||||||
address, chainid: net().chainid,
|
address,
|
||||||
|
chainid: net().chainid,
|
||||||
})
|
})
|
||||||
transactions = txs ?? []
|
transactions = txs ?? []
|
||||||
}
|
}
|
||||||
@@ -354,34 +407,92 @@ export const EvmWallet = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get accountType() { return accountType },
|
get accountType() {
|
||||||
get ready() { return ready },
|
return accountType
|
||||||
get biometricAvailable() { return biometricAvailable },
|
},
|
||||||
get seed() { return seed },
|
get ready() {
|
||||||
get address() { return address },
|
return ready
|
||||||
get balance() { return balance },
|
},
|
||||||
get tokenBalances() { return tokenBalances },
|
get biometricAvailable() {
|
||||||
get network() { return network },
|
return biometricAvailable
|
||||||
get chainid() { return net().chainid },
|
},
|
||||||
get networkName() { return net().name },
|
get seed() {
|
||||||
get symbol() { return net().symbol },
|
return seed
|
||||||
get explorerUrl() { return net().explorerUrl },
|
},
|
||||||
get explorerAddressUrl() { return address ? `${net().explorerUrl.replace('/tx/', '/address/')}${address}` : '' },
|
get address() {
|
||||||
get networks() { return networks },
|
return address
|
||||||
get apiKey() { return apiKey },
|
},
|
||||||
get transactions() { return transactions },
|
get balance() {
|
||||||
get loading() { return loadingBalance || loadingTransactions },
|
return balance
|
||||||
get loadingBalance() { return loadingBalance },
|
},
|
||||||
get loadingTransactions() { return loadingTransactions },
|
get tokenBalances() {
|
||||||
get error() { return error },
|
return tokenBalances
|
||||||
get wallets() { return wallets },
|
},
|
||||||
get currentWalletId() { return currentWalletId },
|
get network() {
|
||||||
get currentWallet() { return wallets.find((w) => w.id === currentWalletId) ?? null },
|
return network
|
||||||
get isLocked() { return !!currentWalletId && !seed },
|
},
|
||||||
get hasWallets() { return wallets.length > 0 },
|
get chainid() {
|
||||||
get currentPasswordHash() { return currentPasswordHash },
|
return net().chainid
|
||||||
set apiKey(v: string) { apiKey = v },
|
},
|
||||||
set error(v: string) { error = v },
|
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,
|
init,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
@@ -401,6 +512,7 @@ export const EvmWallet = () => {
|
|||||||
deleteWallet,
|
deleteWallet,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
syncTxHistory,
|
syncTxHistory,
|
||||||
|
flushTxCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
let confirm = $state(false)
|
let confirm = $state(false)
|
||||||
let resetting = $state(false)
|
let resetting = $state(false)
|
||||||
let deletingWalletId = $state<string | null>(null)
|
let deletingWalletId = $state<string | null>(null)
|
||||||
|
let flushing = $state(false)
|
||||||
|
|
||||||
async function handleReset() {
|
async function handleReset() {
|
||||||
resetting = true
|
resetting = true
|
||||||
@@ -31,6 +32,12 @@
|
|||||||
deletingWalletId = null
|
deletingWalletId = null
|
||||||
if (w.wallets.length === 0) navigate('/multicoin')
|
if (w.wallets.length === 0) navigate('/multicoin')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleFlushCache() {
|
||||||
|
flushing = true
|
||||||
|
await w.flushTxCache()
|
||||||
|
flushing = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto mt-16 max-w-md">
|
||||||
@@ -84,6 +91,17 @@
|
|||||||
<CardDescription>App preferences and data management</CardDescription>
|
<CardDescription>App preferences and data management</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-xs">Transaction Cache</p>
|
||||||
|
<p class="text-muted-foreground text-xs mt-1">
|
||||||
|
Clears all cached transaction history and sync status. Data will be re-fetched from Alchemy on next refresh.
|
||||||
|
</p>
|
||||||
|
<div class="mt-3">
|
||||||
|
<Button variant="outline" onclick={handleFlushCache} disabled={flushing}>
|
||||||
|
{flushing ? 'Flushing...' : 'Flush Transaction Cache'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-xs">Reset App</p>
|
<p class="font-medium text-xs">Reset App</p>
|
||||||
<p class="text-muted-foreground text-xs mt-1">
|
<p class="text-muted-foreground text-xs mt-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user