Switch token price lookup from symbol to address for ERC-20 tokens; keep symbol for native
This commit is contained in:
@@ -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<string, string> = {
|
||||
'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<RPC>({
|
||||
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<RPC>({
|
||||
.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<RPC>({
|
||||
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<RPC>({
|
||||
)
|
||||
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<RPC>({
|
||||
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 }) => {
|
||||
|
||||
@@ -81,7 +81,49 @@
|
||||
return `/multicoin/${id}`
|
||||
}
|
||||
|
||||
const CHAIN_NETWORKS: Record<string, string> = {
|
||||
'1': 'eth-mainnet',
|
||||
'137': 'polygon-mainnet',
|
||||
'56': 'bnb-mainnet',
|
||||
}
|
||||
|
||||
let initStarted = false
|
||||
let nativePrice = $state<{
|
||||
value: string
|
||||
currency: string
|
||||
} | null>(null)
|
||||
let tokenPrices = $state<Record<string, { value: string; currency: string }>>({})
|
||||
|
||||
$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<string, { value: string; currency: string }> = {}
|
||||
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}
|
||||
</p>
|
||||
{#if nativePrice}
|
||||
{@const balVal = Number(w.balance) * Number(nativePrice.value)}
|
||||
<p class="font-mono text-xs text-muted-foreground">
|
||||
${balVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} @ ${Number(nativePrice.value).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}/{w.symbol}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-4 space-y-1">
|
||||
<p class="font-medium text-xs">Tokens</p>
|
||||
{#each w.tokenBalances as t}
|
||||
<p class="flex items-center gap-1.5 font-mono text-sm">
|
||||
<div class="flex items-center gap-1.5 font-mono text-sm">
|
||||
<img
|
||||
src={t.logo}
|
||||
alt=""
|
||||
@@ -359,7 +407,14 @@
|
||||
? '<0.0001'
|
||||
: Number(t.balance).toFixed(4)}
|
||||
{t.symbol}
|
||||
</p>
|
||||
</div>
|
||||
{#if t.contractAddress && tokenPrices[t.contractAddress.toLowerCase()]}
|
||||
{@const val = tokenPrices[t.contractAddress.toLowerCase()]}
|
||||
{@const balVal = Number(t.balance) * Number(val.value)}
|
||||
<p class="font-mono text-xs text-muted-foreground -mt-0.5 ml-6">
|
||||
${balVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} @ ${Number(val.value).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}/{t.symbol}
|
||||
</p>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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: {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user