From f0b567e895e70ca629354e12d2d8fa2cb6c63783 Mon Sep 17 00:00:00 2001
From: TZGyn
Date: Mon, 25 May 2026 15:03:41 +0800
Subject: [PATCH] Switch token price lookup from symbol to address for ERC-20
tokens; keep symbol for native
---
apps/desktop/src/bun/index.ts | 118 ++++++++++++------
.../src/lib/components/EvmDashboard.svelte | 59 ++++++++-
apps/desktop/src/lib/rpc-schema.ts | 10 ++
3 files changed, 146 insertions(+), 41 deletions(-)
diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts
index f96d1ca..81a8e6a 100644
--- a/apps/desktop/src/bun/index.ts
+++ b/apps/desktop/src/bun/index.ts
@@ -38,7 +38,12 @@ import {
evmWallets,
} from './lib/db/schema'
import { syncTxDetails } from './lib/sync/sync-tx-details'
-import type { RPC, TokenBalanceResult, TxEntry } from '../lib/rpc-schema'
+import type {
+ RPC,
+ TokenBalanceResult,
+ TokenPriceEntry,
+ TxEntry,
+} from '../lib/rpc-schema'
const env = getENV()
@@ -110,8 +115,6 @@ const NATIVE_SYMBOLS: Record = {
'56': 'BNB',
}
-
-
async function fetchAlchemyTransfers(
key: string,
chainid: string,
@@ -227,11 +230,10 @@ async function pairTransfers(
for (const [, group] of byHash) {
const native = group.filter(
- (t: any) => t.category === 'external' || t.category === 'internal',
- )
- const tokens = group.filter(
- (t: any) => t.category === 'erc20',
+ (t: any) =>
+ t.category === 'external' || t.category === 'internal',
)
+ const tokens = group.filter((t: any) => t.category === 'erc20')
const extOut = native.filter(
(t: any) => t.from?.toLowerCase() === addrLower,
@@ -253,8 +255,7 @@ async function pairTransfers(
tokenDecimal: tokIn[0].rawContract?.decimal
? String(parseInt(tokIn[0].rawContract.decimal, 16))
: undefined,
- contractAddress:
- tokIn[0].rawContract?.address ?? undefined,
+ contractAddress: tokIn[0].rawContract?.address ?? undefined,
pairedValue: String(tokIn[0].value),
pairedSymbol: tokIn[0].asset,
pairedDecimals: tokIn[0].rawContract?.decimal
@@ -282,9 +283,7 @@ async function pairTransfers(
combined.push({
...mapTransfer(tokOut[0]),
pairedValue: String(extIn[0].value),
- pairedSymbol: chainid
- ? NATIVE_SYMBOLS[chainid]
- : undefined,
+ pairedSymbol: chainid ? NATIVE_SYMBOLS[chainid] : undefined,
})
} else if (tokOut.length > 0) {
combined.push(mapTransfer(tokOut[0]))
@@ -305,15 +304,12 @@ async function pairTransfers(
await Promise.all(
Array.from(addrSet).map(async (addr) => {
const meta = await getTokenMetadata(key, chainid, addr)
- if (meta?.result?.logo)
- logoMap.set(addr, meta.result.logo)
+ if (meta?.result?.logo) logoMap.set(addr, meta.result.logo)
}),
)
for (const entry of combined) {
if (entry.contractAddress)
- entry.logo = logoMap.get(
- entry.contractAddress.toLowerCase(),
- )
+ entry.logo = logoMap.get(entry.contractAddress.toLowerCase())
if (entry.pairedContractAddress)
entry.pairedLogo = logoMap.get(
entry.pairedContractAddress.toLowerCase(),
@@ -321,9 +317,7 @@ async function pairTransfers(
}
}
- combined.sort(
- (a, b) => Number(b.timeStamp) - Number(a.timeStamp),
- )
+ combined.sort((a, b) => Number(b.timeStamp) - Number(a.timeStamp))
return combined
}
@@ -497,6 +491,55 @@ const rpc = BrowserView.defineRPC({
return []
}
},
+ fetchTokenPrices: async ({ symbols, addresses }) => {
+ const key = await Bun.secrets.get({
+ service: 'koins',
+ name: 'alchemy_key',
+ })
+ if (!key) return []
+ try {
+ const results: TokenPriceEntry[] = []
+
+ if (symbols && symbols.length > 0) {
+ const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-symbol?${symbols.map((s) => `symbols=${encodeURIComponent(s)}`).join('&')}`
+ const res = await fetch(url, { signal: AbortSignal.timeout(10000) })
+ if (res.ok) {
+ const body = await res.json()
+ const data = (body.data ?? []) as any[]
+ for (const d of data) {
+ for (const p of d.prices ?? []) {
+ results.push({ symbol: d.symbol, currency: p.currency, value: p.value, lastUpdatedAt: p.lastUpdatedAt })
+ }
+ }
+ }
+ }
+
+ if (addresses && addresses.length > 0) {
+ const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-address`
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ addresses }),
+ signal: AbortSignal.timeout(10000),
+ })
+ if (res.ok) {
+ const body = await res.json()
+ const data = (body.data ?? []) as any[]
+ for (const d of data) {
+ for (const p of d.prices ?? []) {
+ results.push({ symbol: d.symbol, currency: p.currency, value: p.value, lastUpdatedAt: p.lastUpdatedAt, network: d.network, address: d.address })
+ }
+ }
+ }
+ }
+
+ console.log('[prices] results:', results.length)
+ return results
+ } catch (error) {
+ console.log('[prices] error:', error)
+ return []
+ }
+ },
fetchTransactionDetails: async ({ hash, chainid, address }) => {
const key = await Bun.secrets.get({
service: 'koins',
@@ -520,20 +563,11 @@ const rpc = BrowserView.defineRPC({
.all()
if (transfers.length > 0) {
- const paired = await pairTransfers(
- transfers,
- address,
- )
- if (
- paired.length > 0 &&
- paired[0].pairedValue
- ) {
- details.pairedValue =
- paired[0].pairedValue
- details.pairedSymbol =
- paired[0].pairedSymbol
- details.pairedDecimals =
- paired[0].pairedDecimals
+ const paired = await pairTransfers(transfers, address)
+ if (paired.length > 0 && paired[0].pairedValue) {
+ details.pairedValue = paired[0].pairedValue
+ details.pairedSymbol = paired[0].pairedSymbol
+ details.pairedDecimals = paired[0].pairedDecimals
details.pairedContractAddress =
paired[0].pairedContractAddress
details.pairedLogo = paired[0].pairedLogo
@@ -666,9 +700,7 @@ const rpc = BrowserView.defineRPC({
if (!moneroManager)
throw new Error('Monero wallet RPC not started')
const txs = await moneroManager.getTransactions(accountIndex)
- console.log(
- `[rpc] moneroGetTransactions: ${txs.length} txs`,
- )
+ console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
return txs
},
moneroWalletStatus: async () => {
@@ -710,7 +742,12 @@ const rpc = BrowserView.defineRPC({
)
return wallets
},
- moneroTransfer: async ({ address, amount, priority, accountIndex }) => {
+ moneroTransfer: async ({
+ address,
+ amount,
+ priority,
+ accountIndex,
+ }) => {
console.log('[rpc] moneroTransfer:', {
address,
amount,
@@ -732,7 +769,10 @@ const rpc = BrowserView.defineRPC({
if (!moneroManager)
throw new Error('Monero wallet RPC not started')
const details = await moneroManager.getTransferDetails(txid)
- console.log('[rpc] moneroGetTransferDetails complete:', details?.hash)
+ console.log(
+ '[rpc] moneroGetTransferDetails complete:',
+ details?.hash,
+ )
return details
},
evmCreateWallet: async ({ name, phrase, passwordHash }) => {
diff --git a/apps/desktop/src/lib/components/EvmDashboard.svelte b/apps/desktop/src/lib/components/EvmDashboard.svelte
index 49a8e01..ba5c253 100644
--- a/apps/desktop/src/lib/components/EvmDashboard.svelte
+++ b/apps/desktop/src/lib/components/EvmDashboard.svelte
@@ -81,7 +81,49 @@
return `/multicoin/${id}`
}
+ const CHAIN_NETWORKS: Record = {
+ '1': 'eth-mainnet',
+ '137': 'polygon-mainnet',
+ '56': 'bnb-mainnet',
+ }
+
let initStarted = false
+ let nativePrice = $state<{
+ value: string
+ currency: string
+ } | null>(null)
+ let tokenPrices = $state>({})
+
+ $effect(() => {
+ const syms: string[] = w.symbol ? [w.symbol] : []
+ const addrs: { network: string; address: string }[] = []
+ const network = CHAIN_NETWORKS[w.chainid]
+ if (network) {
+ for (const t of w.tokenBalances) {
+ if (t.contractAddress) {
+ addrs.push({ network, address: t.contractAddress })
+ }
+ }
+ }
+ if (syms.length > 0 || addrs.length > 0) {
+ electrobun.rpc?.request
+ .fetchTokenPrices({ symbols: syms.length > 0 ? syms : undefined, addresses: addrs.length > 0 ? addrs : undefined })
+ .then((prices) => {
+ const usd = prices.find((p) => p.currency === 'usd' && p.symbol === w.symbol && !p.address)
+ nativePrice = usd
+ ? { value: usd.value, currency: usd.currency }
+ : null
+ const map: Record = {}
+ for (const p of prices) {
+ if (p.currency === 'usd' && p.address) {
+ map[p.address.toLowerCase()] = { value: p.value, currency: p.currency }
+ }
+ }
+ tokenPrices = map
+ })
+ }
+ })
+
$effect(() => {
if (!initStarted) {
initStarted = true
@@ -343,11 +385,17 @@
{w.balance}
{w.symbol}
+ {#if nativePrice}
+ {@const balVal = Number(w.balance) * Number(nativePrice.value)}
+
+ ${balVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} @ ${Number(nativePrice.value).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}/{w.symbol}
+
+ {/if}
Tokens
{#each w.tokenBalances as t}
-
+

+
+ {#if t.contractAddress && tokenPrices[t.contractAddress.toLowerCase()]}
+ {@const val = tokenPrices[t.contractAddress.toLowerCase()]}
+ {@const balVal = Number(t.balance) * Number(val.value)}
+
+ ${balVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} @ ${Number(val.value).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}/{t.symbol}
+
+ {/if}
{/each}
{/if}
diff --git a/apps/desktop/src/lib/rpc-schema.ts b/apps/desktop/src/lib/rpc-schema.ts
index 074a10b..9f6a886 100644
--- a/apps/desktop/src/lib/rpc-schema.ts
+++ b/apps/desktop/src/lib/rpc-schema.ts
@@ -116,6 +116,15 @@ export type TokenBalanceResult = {
logo?: string
}
+export type TokenPriceEntry = {
+ symbol: string
+ currency: string
+ value: string
+ lastUpdatedAt: string
+ network?: string
+ address?: string
+}
+
export type EvmWalletInfo = {
id: string
name: string
@@ -159,6 +168,7 @@ export type RPC = {
evmListWallets: { params: {}; response: EvmWalletInfo[] }
evmGetSeed: { params: { vaultKey: string }; response: string }
evmDeleteWallet: { params: { id: string }; response: void }
+ fetchTokenPrices: { params: { symbols?: string[]; addresses?: { network: string; address: string }[] }; response: TokenPriceEntry[] }
}
messages: {}
}