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: {} }