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,
|
evmWallets,
|
||||||
} from './lib/db/schema'
|
} from './lib/db/schema'
|
||||||
import { syncTxDetails } from './lib/sync/sync-tx-details'
|
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()
|
const env = getENV()
|
||||||
|
|
||||||
@@ -110,8 +115,6 @@ const NATIVE_SYMBOLS: Record<string, string> = {
|
|||||||
'56': 'BNB',
|
'56': 'BNB',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function fetchAlchemyTransfers(
|
async function fetchAlchemyTransfers(
|
||||||
key: string,
|
key: string,
|
||||||
chainid: string,
|
chainid: string,
|
||||||
@@ -227,11 +230,10 @@ async function pairTransfers(
|
|||||||
|
|
||||||
for (const [, group] of byHash) {
|
for (const [, group] of byHash) {
|
||||||
const native = group.filter(
|
const native = group.filter(
|
||||||
(t: any) => t.category === 'external' || t.category === 'internal',
|
(t: any) =>
|
||||||
)
|
t.category === 'external' || t.category === 'internal',
|
||||||
const tokens = group.filter(
|
|
||||||
(t: any) => t.category === 'erc20',
|
|
||||||
)
|
)
|
||||||
|
const tokens = group.filter((t: any) => t.category === 'erc20')
|
||||||
|
|
||||||
const extOut = native.filter(
|
const extOut = native.filter(
|
||||||
(t: any) => t.from?.toLowerCase() === addrLower,
|
(t: any) => t.from?.toLowerCase() === addrLower,
|
||||||
@@ -253,8 +255,7 @@ async function pairTransfers(
|
|||||||
tokenDecimal: tokIn[0].rawContract?.decimal
|
tokenDecimal: tokIn[0].rawContract?.decimal
|
||||||
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
||||||
: undefined,
|
: undefined,
|
||||||
contractAddress:
|
contractAddress: tokIn[0].rawContract?.address ?? undefined,
|
||||||
tokIn[0].rawContract?.address ?? undefined,
|
|
||||||
pairedValue: String(tokIn[0].value),
|
pairedValue: String(tokIn[0].value),
|
||||||
pairedSymbol: tokIn[0].asset,
|
pairedSymbol: tokIn[0].asset,
|
||||||
pairedDecimals: tokIn[0].rawContract?.decimal
|
pairedDecimals: tokIn[0].rawContract?.decimal
|
||||||
@@ -282,9 +283,7 @@ async function pairTransfers(
|
|||||||
combined.push({
|
combined.push({
|
||||||
...mapTransfer(tokOut[0]),
|
...mapTransfer(tokOut[0]),
|
||||||
pairedValue: String(extIn[0].value),
|
pairedValue: String(extIn[0].value),
|
||||||
pairedSymbol: chainid
|
pairedSymbol: chainid ? NATIVE_SYMBOLS[chainid] : undefined,
|
||||||
? NATIVE_SYMBOLS[chainid]
|
|
||||||
: undefined,
|
|
||||||
})
|
})
|
||||||
} else if (tokOut.length > 0) {
|
} else if (tokOut.length > 0) {
|
||||||
combined.push(mapTransfer(tokOut[0]))
|
combined.push(mapTransfer(tokOut[0]))
|
||||||
@@ -305,15 +304,12 @@ async function pairTransfers(
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(addrSet).map(async (addr) => {
|
Array.from(addrSet).map(async (addr) => {
|
||||||
const meta = await getTokenMetadata(key, chainid, addr)
|
const meta = await getTokenMetadata(key, chainid, addr)
|
||||||
if (meta?.result?.logo)
|
if (meta?.result?.logo) logoMap.set(addr, meta.result.logo)
|
||||||
logoMap.set(addr, meta.result.logo)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
for (const entry of combined) {
|
for (const entry of combined) {
|
||||||
if (entry.contractAddress)
|
if (entry.contractAddress)
|
||||||
entry.logo = logoMap.get(
|
entry.logo = logoMap.get(entry.contractAddress.toLowerCase())
|
||||||
entry.contractAddress.toLowerCase(),
|
|
||||||
)
|
|
||||||
if (entry.pairedContractAddress)
|
if (entry.pairedContractAddress)
|
||||||
entry.pairedLogo = logoMap.get(
|
entry.pairedLogo = logoMap.get(
|
||||||
entry.pairedContractAddress.toLowerCase(),
|
entry.pairedContractAddress.toLowerCase(),
|
||||||
@@ -321,9 +317,7 @@ async function pairTransfers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
combined.sort(
|
combined.sort((a, b) => Number(b.timeStamp) - Number(a.timeStamp))
|
||||||
(a, b) => Number(b.timeStamp) - Number(a.timeStamp),
|
|
||||||
)
|
|
||||||
|
|
||||||
return combined
|
return combined
|
||||||
}
|
}
|
||||||
@@ -497,6 +491,55 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
return []
|
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 }) => {
|
fetchTransactionDetails: async ({ hash, chainid, address }) => {
|
||||||
const key = await Bun.secrets.get({
|
const key = await Bun.secrets.get({
|
||||||
service: 'koins',
|
service: 'koins',
|
||||||
@@ -520,20 +563,11 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
.all()
|
.all()
|
||||||
|
|
||||||
if (transfers.length > 0) {
|
if (transfers.length > 0) {
|
||||||
const paired = await pairTransfers(
|
const paired = await pairTransfers(transfers, address)
|
||||||
transfers,
|
if (paired.length > 0 && paired[0].pairedValue) {
|
||||||
address,
|
details.pairedValue = paired[0].pairedValue
|
||||||
)
|
details.pairedSymbol = paired[0].pairedSymbol
|
||||||
if (
|
details.pairedDecimals = paired[0].pairedDecimals
|
||||||
paired.length > 0 &&
|
|
||||||
paired[0].pairedValue
|
|
||||||
) {
|
|
||||||
details.pairedValue =
|
|
||||||
paired[0].pairedValue
|
|
||||||
details.pairedSymbol =
|
|
||||||
paired[0].pairedSymbol
|
|
||||||
details.pairedDecimals =
|
|
||||||
paired[0].pairedDecimals
|
|
||||||
details.pairedContractAddress =
|
details.pairedContractAddress =
|
||||||
paired[0].pairedContractAddress
|
paired[0].pairedContractAddress
|
||||||
details.pairedLogo = paired[0].pairedLogo
|
details.pairedLogo = paired[0].pairedLogo
|
||||||
@@ -666,9 +700,7 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
if (!moneroManager)
|
if (!moneroManager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
const txs = await moneroManager.getTransactions(accountIndex)
|
const txs = await moneroManager.getTransactions(accountIndex)
|
||||||
console.log(
|
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
||||||
`[rpc] moneroGetTransactions: ${txs.length} txs`,
|
|
||||||
)
|
|
||||||
return txs
|
return txs
|
||||||
},
|
},
|
||||||
moneroWalletStatus: async () => {
|
moneroWalletStatus: async () => {
|
||||||
@@ -710,7 +742,12 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
)
|
)
|
||||||
return wallets
|
return wallets
|
||||||
},
|
},
|
||||||
moneroTransfer: async ({ address, amount, priority, accountIndex }) => {
|
moneroTransfer: async ({
|
||||||
|
address,
|
||||||
|
amount,
|
||||||
|
priority,
|
||||||
|
accountIndex,
|
||||||
|
}) => {
|
||||||
console.log('[rpc] moneroTransfer:', {
|
console.log('[rpc] moneroTransfer:', {
|
||||||
address,
|
address,
|
||||||
amount,
|
amount,
|
||||||
@@ -732,7 +769,10 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
if (!moneroManager)
|
if (!moneroManager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
const details = await moneroManager.getTransferDetails(txid)
|
const details = await moneroManager.getTransferDetails(txid)
|
||||||
console.log('[rpc] moneroGetTransferDetails complete:', details?.hash)
|
console.log(
|
||||||
|
'[rpc] moneroGetTransferDetails complete:',
|
||||||
|
details?.hash,
|
||||||
|
)
|
||||||
return details
|
return details
|
||||||
},
|
},
|
||||||
evmCreateWallet: async ({ name, phrase, passwordHash }) => {
|
evmCreateWallet: async ({ name, phrase, passwordHash }) => {
|
||||||
|
|||||||
@@ -81,7 +81,49 @@
|
|||||||
return `/multicoin/${id}`
|
return `/multicoin/${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CHAIN_NETWORKS: Record<string, string> = {
|
||||||
|
'1': 'eth-mainnet',
|
||||||
|
'137': 'polygon-mainnet',
|
||||||
|
'56': 'bnb-mainnet',
|
||||||
|
}
|
||||||
|
|
||||||
let initStarted = false
|
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(() => {
|
$effect(() => {
|
||||||
if (!initStarted) {
|
if (!initStarted) {
|
||||||
initStarted = true
|
initStarted = true
|
||||||
@@ -343,11 +385,17 @@
|
|||||||
{w.balance}
|
{w.balance}
|
||||||
{w.symbol}
|
{w.symbol}
|
||||||
</p>
|
</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>
|
||||||
<div class="mb-4 space-y-1">
|
<div class="mb-4 space-y-1">
|
||||||
<p class="font-medium text-xs">Tokens</p>
|
<p class="font-medium text-xs">Tokens</p>
|
||||||
{#each w.tokenBalances as t}
|
{#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
|
<img
|
||||||
src={t.logo}
|
src={t.logo}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -359,7 +407,14 @@
|
|||||||
? '<0.0001'
|
? '<0.0001'
|
||||||
: Number(t.balance).toFixed(4)}
|
: Number(t.balance).toFixed(4)}
|
||||||
{t.symbol}
|
{t.symbol}
|
||||||
|
</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>
|
</p>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -116,6 +116,15 @@ export type TokenBalanceResult = {
|
|||||||
logo?: string
|
logo?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TokenPriceEntry = {
|
||||||
|
symbol: string
|
||||||
|
currency: string
|
||||||
|
value: string
|
||||||
|
lastUpdatedAt: string
|
||||||
|
network?: string
|
||||||
|
address?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type EvmWalletInfo = {
|
export type EvmWalletInfo = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -159,6 +168,7 @@ export type RPC = {
|
|||||||
evmListWallets: { params: {}; response: EvmWalletInfo[] }
|
evmListWallets: { params: {}; response: EvmWalletInfo[] }
|
||||||
evmGetSeed: { params: { vaultKey: string }; response: string }
|
evmGetSeed: { params: { vaultKey: string }; response: string }
|
||||||
evmDeleteWallet: { params: { id: string }; response: void }
|
evmDeleteWallet: { params: { id: string }; response: void }
|
||||||
|
fetchTokenPrices: { params: { symbols?: string[]; addresses?: { network: string; address: string }[] }; response: TokenPriceEntry[] }
|
||||||
}
|
}
|
||||||
messages: {}
|
messages: {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user