filter out tokens without logo on tokens list

This commit is contained in:
2026-05-11 16:31:48 +08:00
parent 45402098b8
commit d2c2a46e54
4 changed files with 324 additions and 356 deletions
+217 -135
View File
@@ -7,6 +7,7 @@ import {
type RPCSchema, type RPCSchema,
Utils, Utils,
} from 'electrobun/bun' } from 'electrobun/bun'
import { inspect } from 'util'
const DEV_SERVER_PORT = 5173 const DEV_SERVER_PORT = 5173
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}` const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
@@ -56,7 +57,18 @@ ApplicationMenu.setApplicationMenu([
const url = await getMainViewUrl() const url = await getMainViewUrl()
const ETHERSCAN_API = 'https://api.etherscan.io/v2/api' const ALCHEMY_NETWORKS: Record<string, string> = {
'1': 'eth-mainnet',
'137': 'polygon-mainnet',
'56': 'bnb-mainnet',
}
type TokenBalanceResult = {
symbol: string
balance: string
contractAddress: string
logo?: string
}
type TxEntry = { type TxEntry = {
hash: string hash: string
@@ -64,10 +76,7 @@ type TxEntry = {
from: string from: string
to: string to: string
value: string value: string
input?: string
isError?: string
tokenSymbol?: string tokenSymbol?: string
tokenName?: string
tokenDecimal?: string tokenDecimal?: string
contractAddress?: string contractAddress?: string
pairedValue?: string pairedValue?: string
@@ -97,11 +106,16 @@ type RPC = {
params: { params: {
address: string address: string
chainid: string chainid: string
page?: number
offset?: number
} }
response: Promise<TxEntry[]> response: Promise<TxEntry[]>
} }
fetchTokenBalances: {
params: {
address: string
chainid: string
}
response: Promise<TokenBalanceResult[]>
}
openExternal: { openExternal: {
params: { params: {
url: string url: string
@@ -117,32 +131,148 @@ type RPC = {
}> }>
} }
async function fetchEtherscan( async function fetchAlchemyTransfers(
key: string, key: string,
chainid: string, chainid: string,
action: string,
address: string, address: string,
page: number,
offset: number,
): Promise<any[]> { ): Promise<any[]> {
const params = new URLSearchParams({ const network = ALCHEMY_NETWORKS[chainid]
module: 'account', if (!network) return []
action, const url = `https://${network}.g.alchemy.com/v2/${key}`
address,
page: String(page), const body = (fromAddress?: string, toAddress?: string) => ({
offset: String(offset), jsonrpc: '2.0',
sort: 'desc', id: 0,
apikey: key, method: 'alchemy_getAssetTransfers',
chainid, params: [
{
...(fromAddress ? { fromAddress } : {}),
...(toAddress ? { toAddress } : {}),
category: ['external', 'erc20'],
withMetadata: true,
maxCount: '0x3E8',
},
],
}) })
const res = await fetch(`${ETHERSCAN_API}?${params}`)
const data = await res.json() const [outRes, inRes] = await Promise.all([
if (data.status !== '1') return [] fetch(url, {
return data.result method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body(address, undefined)),
}).then((r) => r.json()),
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body(undefined, address)),
}).then((r) => r.json()),
])
const outgoing: any[] = outRes.result?.transfers ?? []
const incoming: any[] = inRes.result?.transfers ?? []
const seen = new Set<string>()
return [...outgoing, ...incoming].filter((t) => {
if (seen.has(t.hash)) return false
seen.add(t.hash)
return true
})
}
function formatUnits(balance: string, decimals: number): string {
const bal = BigInt(balance)
if (bal === 0n) return '0'
const divisor = 10n ** BigInt(decimals)
const intPart = bal / divisor
const fracPart = (bal % divisor)
.toString()
.padStart(decimals, '0')
.replace(/0+$/, '')
return fracPart ? `${intPart}.${fracPart}` : `${intPart}`
}
async function fetchAlchemyTokenBalances(
key: string,
chainid: string,
address: string,
): Promise<TokenBalanceResult[]> {
const network = ALCHEMY_NETWORKS[chainid]
if (!network) return []
const url = `https://${network}.g.alchemy.com/v2/${key}`
const post = (body: any) =>
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then((r) => r.json())
const balRes = await post({
jsonrpc: '2.0',
id: 0,
method: 'alchemy_getTokenBalances',
params: [address, 'erc20'],
})
// console.log(inspect(balRes, { depth: null }))
const tokens: any[] = balRes.result?.tokenBalances ?? []
const nonZero = tokens.filter(
(t: any) => t.tokenBalance !== '0x0' && !t.error,
)
const metas = await Promise.allSettled(
nonZero.map((t: any) =>
post({
jsonrpc: '2.0',
id: 0,
method: 'alchemy_getTokenMetadata',
params: [t.contractAddress],
}),
),
)
console.log(inspect(metas, { depth: null }))
return nonZero
.map((t: any, i: number) => {
const meta =
metas[i].status === 'fulfilled' ? metas[i].value.result : null
return {
symbol: meta?.symbol ?? 'Unknown',
decimals: meta?.decimals ?? 0,
balance: formatUnits(t.tokenBalance, meta?.decimals ?? 18),
contractAddress: t.contractAddress,
logo: meta?.logo ?? undefined,
}
})
.filter((t) => t.logo && t.decimals !== 0)
}
function mapTransfer(t: any): TxEntry {
const isExternal = t.category === 'external'
return {
hash: t.hash,
timeStamp: String(
Math.floor(
new Date(t.metadata.blockTimestamp).getTime() / 1000,
),
),
from: t.from,
to: t.to,
value: String(t.value),
...(isExternal
? {}
: {
tokenSymbol: t.asset,
tokenDecimal: t.rawContract?.decimal
? String(parseInt(t.rawContract.decimal, 16))
: undefined,
contractAddress: t.rawContract?.address ?? undefined,
}),
}
} }
const rpc = BrowserView.defineRPC<RPC>({ const rpc = BrowserView.defineRPC<RPC>({
maxRequestTime: 20000, maxRequestTime: 200000,
handlers: { handlers: {
requests: { requests: {
getSecret: async ({ name, service }) => { getSecret: async ({ name, service }) => {
@@ -154,140 +284,78 @@ const rpc = BrowserView.defineRPC<RPC>({
openExternal: async ({ url }) => { openExternal: async ({ url }) => {
Utils.openExternal(url) Utils.openExternal(url)
}, },
fetchTxHistory: async ({ fetchTxHistory: async ({ address, chainid }) => {
address,
chainid,
page = 1,
offset = 1000,
}) => {
const key = await Bun.secrets.get({ const key = await Bun.secrets.get({
service: 'koins', service: 'koins',
name: 'etherscan_key', name: 'alchemy_key',
}) })
if (!key) return [] if (!key) return []
const [nativeTxs, tokenTxs] = await Promise.all([ const all = await fetchAlchemyTransfers(key, chainid, address)
fetchEtherscan(
key,
chainid,
'txlist',
address,
page,
offset,
),
fetchEtherscan(
key,
chainid,
'tokentx',
address,
page,
offset,
),
])
const addrLower = address.toLowerCase() const addrLower = address.toLowerCase()
const byHash = new Map< const byHash = new Map<string, any[]>()
string, for (const t of all) {
{ native?: any; token: any[] }
>()
for (const t of nativeTxs) {
const h = t.hash const h = t.hash
if (!byHash.has(h)) byHash.set(h, { token: [] }) if (!byHash.has(h)) byHash.set(h, [])
byHash.get(h)!.native = t byHash.get(h)!.push(t)
}
for (const t of tokenTxs) {
const h = t.hash
if (!byHash.has(h)) byHash.set(h, { token: [] })
byHash.get(h)!.token.push(t)
} }
const combined: TxEntry[] = [] const combined: TxEntry[] = []
for (const [, group] of byHash) { for (const [, group] of byHash) {
console.log(group) const external = group.filter(
const { native, token } = group (t: any) => t.category === 'external',
const nv = native?.value ?? '0' )
const tokens = group.filter(
(t: any) => t.category === 'erc20',
)
const incoming = token.filter( const extOut = external.filter(
(t: any) => t.from.toLowerCase() === addrLower,
)
const extIn = external.filter(
(t: any) => t.to.toLowerCase() === addrLower, (t: any) => t.to.toLowerCase() === addrLower,
) )
const outgoing = token.filter( const tokIn = tokens.filter(
(t: any) => t.to.toLowerCase() === addrLower,
)
const tokOut = tokens.filter(
(t: any) => t.from.toLowerCase() === addrLower, (t: any) => t.from.toLowerCase() === addrLower,
) )
if (native && nv !== '0' && incoming.length > 0) { if (extOut.length > 0 && tokIn.length > 0) {
combined.push({ combined.push({
hash: native.hash, ...mapTransfer(extOut[0]),
timeStamp: native.timeStamp, tokenSymbol: tokIn[0].asset,
from: native.from, tokenDecimal: tokIn[0].rawContract?.decimal
to: native.to, ? String(parseInt(tokIn[0].rawContract.decimal, 16))
value: native.value, : undefined,
input: native.input, contractAddress:
isError: native.isError, tokIn[0].rawContract?.address ?? undefined,
tokenSymbol: incoming[0].tokenSymbol, pairedValue: String(tokIn[0].value),
tokenDecimal: incoming[0].tokenDecimal, pairedSymbol: tokIn[0].asset,
contractAddress: incoming[0].contractAddress, pairedDecimals: tokIn[0].rawContract?.decimal
pairedValue: incoming[0].value, ? String(parseInt(tokIn[0].rawContract.decimal, 16))
pairedSymbol: incoming[0].tokenSymbol, : undefined,
pairedDecimals: incoming[0].tokenDecimal,
}) })
} else if (native && nv !== '0') { } else if (extOut.length > 0) {
combined.push(mapTransfer(extOut[0]))
} else if (extIn.length > 0) {
combined.push(mapTransfer(extIn[0]))
} else if (tokOut.length > 0 && tokIn.length > 0) {
combined.push({ combined.push({
hash: native.hash, ...mapTransfer(tokOut[0]),
timeStamp: native.timeStamp, pairedValue: String(tokIn[0].value),
from: native.from, pairedSymbol: tokIn[0].asset,
to: native.to, pairedDecimals: tokIn[0].rawContract?.decimal
value: native.value, ? String(parseInt(tokIn[0].rawContract.decimal, 16))
input: native.input, : undefined,
isError: native.isError,
})
} else if (incoming.length > 0 && outgoing.length > 0) {
combined.push({
hash: incoming[0].hash,
timeStamp: incoming[0].timeStamp,
from: incoming[0].from,
to: incoming[0].to,
value: outgoing[0].value,
tokenSymbol: outgoing[0].tokenSymbol,
tokenDecimal: outgoing[0].tokenDecimal,
contractAddress: outgoing[0].contractAddress,
pairedValue: incoming[0].value,
pairedSymbol: incoming[0].tokenSymbol,
pairedDecimals: incoming[0].tokenDecimal,
})
} else if (outgoing.length > 0) {
combined.push({
hash: outgoing[0].hash,
timeStamp: outgoing[0].timeStamp,
from: outgoing[0].from,
to: outgoing[0].to,
value: outgoing[0].value,
tokenSymbol: outgoing[0].tokenSymbol,
tokenDecimal: outgoing[0].tokenDecimal,
contractAddress: outgoing[0].contractAddress,
isError: '0',
})
} else if (incoming.length > 0) {
combined.push({
hash: incoming[0].hash,
timeStamp: incoming[0].timeStamp,
from: incoming[0].from,
to: incoming[0].to,
value: incoming[0].value,
tokenSymbol: incoming[0].tokenSymbol,
tokenDecimal: incoming[0].tokenDecimal,
contractAddress: incoming[0].contractAddress,
})
} else if (native) {
combined.push({
hash: native.hash,
timeStamp: native.timeStamp,
from: native.from,
to: native.to,
value: native.value,
input: native.input,
isError: native.isError,
}) })
} else if (tokOut.length > 0) {
combined.push(mapTransfer(tokOut[0]))
} else if (tokIn.length > 0) {
combined.push(mapTransfer(tokIn[0]))
} }
} }
@@ -297,6 +365,20 @@ const rpc = BrowserView.defineRPC<RPC>({
return combined return combined
}, },
fetchTokenBalances: async ({ address, chainid }) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) return []
const result = await fetchAlchemyTokenBalances(
key,
chainid,
address,
)
// console.log(inspect(result, { depth: null }))
return result
},
}, },
messages: {}, messages: {},
}, },
+14 -5
View File
@@ -6,10 +6,7 @@ export type TxEntry = {
from: string from: string
to: string to: string
value: string value: string
input?: string
isError?: string
tokenSymbol?: string tokenSymbol?: string
tokenName?: string
tokenDecimal?: string tokenDecimal?: string
contractAddress?: string contractAddress?: string
pairedValue?: string pairedValue?: string
@@ -17,6 +14,13 @@ export type TxEntry = {
pairedDecimals?: string pairedDecimals?: string
} }
export type TokenBalanceResult = {
symbol: string
balance: string
contractAddress: string
logo?: string
}
type RPC = { type RPC = {
bun: { bun: {
requests: { requests: {
@@ -39,11 +43,16 @@ type RPC = {
params: { params: {
address: string address: string
chainid: string chainid: string
page?: number
offset?: number
} }
response: Promise<TxEntry[]> response: Promise<TxEntry[]>
} }
fetchTokenBalances: {
params: {
address: string
chainid: string
}
response: Promise<TokenBalanceResult[]>
}
openExternal: { openExternal: {
params: { params: {
url: string url: string
+18 -171
View File
@@ -2,23 +2,9 @@ import {
JsonRpcProvider, JsonRpcProvider,
HDNodeWallet, HDNodeWallet,
formatEther, formatEther,
formatUnits,
Contract,
} from 'ethers' } from 'ethers'
import { electrobun, type TxEntry } from '$lib/electrobun' import { electrobun, type TxEntry } from '$lib/electrobun'
const ERC20_ABI = [
'function balanceOf(address) view returns (uint256)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
]
type DiscoveredToken = {
address: string
symbol: string
decimals: number
}
export const networks = [ export const networks = [
{ {
id: 'bsc', id: 'bsc',
@@ -27,18 +13,6 @@ export const networks = [
symbol: 'BNB', symbol: 'BNB',
chainid: '56', chainid: '56',
explorerUrl: 'https://bscscan.com/tx/', explorerUrl: 'https://bscscan.com/tx/',
tokens: [
{
symbol: 'USDC',
address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
decimals: 18,
},
{
symbol: 'USDT',
address: '0x55d398326f99059fF775485246999027B3197955',
decimals: 18,
},
],
}, },
{ {
id: 'polygon', id: 'polygon',
@@ -47,18 +21,6 @@ export const networks = [
symbol: 'POL', symbol: 'POL',
chainid: '137', chainid: '137',
explorerUrl: 'https://polygonscan.com/tx/', explorerUrl: 'https://polygonscan.com/tx/',
tokens: [
{
symbol: 'USDC',
address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
decimals: 6,
},
{
symbol: 'USDT',
address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
decimals: 6,
},
],
}, },
{ {
id: 'eth', id: 'eth',
@@ -67,23 +29,6 @@ export const networks = [
symbol: 'ETH', symbol: 'ETH',
chainid: '1', chainid: '1',
explorerUrl: 'https://etherscan.io/tx/', explorerUrl: 'https://etherscan.io/tx/',
tokens: [
{
symbol: 'USDC',
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
decimals: 6,
},
{
symbol: 'USDT',
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
decimals: 6,
},
{
symbol: 'BAT',
address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF',
decimals: 18,
},
],
}, },
] as const ] as const
@@ -92,7 +37,8 @@ export type NetworkId = (typeof networks)[number]['id']
export type TokenBalance = { export type TokenBalance = {
symbol: string symbol: string
balance: string balance: string
address?: string contractAddress: string
logo?: string
} }
export const Wallet = () => { export const Wallet = () => {
@@ -105,9 +51,8 @@ export const Wallet = () => {
let error = $state('') let error = $state('')
let vaultExists = $state(false) let vaultExists = $state(false)
let ready = $state(false) let ready = $state(false)
let etherscanKey = $state('') let apiKey = $state('')
let transactions = $state<TxEntry[]>([]) let transactions = $state<TxEntry[]>([])
let discoveredTokens = $state<DiscoveredToken[]>([])
const net = () => networks.find((n) => n.id === network)! const net = () => networks.find((n) => n.id === network)!
@@ -120,9 +65,9 @@ export const Wallet = () => {
vaultExists = raw !== null && raw !== undefined vaultExists = raw !== null && raw !== undefined
const key = await electrobun.rpc?.request.getSecret({ const key = await electrobun.rpc?.request.getSecret({
service: 'koins', service: 'koins',
name: 'etherscan_key', name: 'alchemy_key',
}) })
if (key) etherscanKey = key if (key) apiKey = key
if (raw) { if (raw) {
seed = raw seed = raw
await refresh() await refresh()
@@ -138,7 +83,6 @@ export const Wallet = () => {
const switchNetwork = async (id: NetworkId) => { const switchNetwork = async (id: NetworkId) => {
network = id network = id
discoveredTokens = []
if (seed) await refresh() if (seed) await refresh()
} }
@@ -169,7 +113,6 @@ export const Wallet = () => {
error = '' error = ''
tokenBalances = [] tokenBalances = []
transactions = [] transactions = []
discoveredTokens = []
} }
const refresh = async () => { const refresh = async () => {
@@ -184,39 +127,12 @@ export const Wallet = () => {
const nativeBal = await provider.getBalance(wallet.address) const nativeBal = await provider.getBalance(wallet.address)
balance = formatEther(nativeBal) balance = formatEther(nativeBal)
const [hardcodedBals, discoveredBals] = await Promise.all([ const bals = await electrobun.rpc?.request.fetchTokenBalances({
Promise.all( address: wallet.address,
net().tokens.map(async (t) => { chainid: net().chainid,
try { })
const c = new Contract(t.address, ERC20_ABI, provider) tokenBalances = bals ?? []
const bal = await c.balanceOf(wallet.address)
return {
symbol: t.symbol,
balance: formatUnits(bal, t.decimals),
address: t.address,
}
} catch {
return { symbol: t.symbol, balance: '0', address: t.address }
}
}),
),
Promise.all(
discoveredTokens.map(async (t) => {
try {
const c = new Contract(t.address, ERC20_ABI, provider)
const bal = await c.balanceOf(wallet.address)
return {
symbol: t.symbol,
balance: formatUnits(bal, t.decimals),
address: t.address,
}
} catch {
return { symbol: t.symbol, balance: '0', address: t.address }
}
}),
),
])
tokenBalances = [...hardcodedBals, ...discoveredBals]
fetchTxHistory() fetchTxHistory()
} catch (e) { } catch (e) {
error = error =
@@ -226,18 +142,18 @@ export const Wallet = () => {
} }
} }
const saveEtherscanKey = async (key: string) => { const saveApiKey = async (key: string) => {
await electrobun.rpc?.request.setSecret({ await electrobun.rpc?.request.setSecret({
service: 'koins', service: 'koins',
name: 'etherscan_key', name: 'alchemy_key',
value: key, value: key,
}) })
etherscanKey = key apiKey = key
if (address) await fetchTxHistory() if (address) await fetchTxHistory()
} }
const fetchTxHistory = async () => { const fetchTxHistory = async () => {
if (!address || !etherscanKey) { if (!address || !apiKey) {
transactions = [] transactions = []
return return
} }
@@ -246,69 +162,6 @@ export const Wallet = () => {
chainid: net().chainid, chainid: net().chainid,
}) })
transactions = txs ?? [] transactions = txs ?? []
const seen = new Set(
net().tokens.map((t) => t.address.toLowerCase()),
)
discoveredTokens.forEach((d) => seen.add(d.address.toLowerCase()))
const newAddrs = [
...new Set(
(txs ?? [])
.filter(
(t) =>
t.contractAddress &&
!seen.has(t.contractAddress.toLowerCase()),
)
.map((t) => t.contractAddress!.toLowerCase()),
),
]
if (newAddrs.length === 0) return
const provider = new JsonRpcProvider(net().rpc)
const newTokens: DiscoveredToken[] = []
for (const addr of newAddrs) {
try {
const c = new Contract(addr, ERC20_ABI, provider)
const [symbol, decimals] = await Promise.all([
c.symbol(),
c.decimals(),
])
if (
typeof symbol !== 'string' ||
symbol.length > 12 ||
/[\s.:/]/.test(symbol)
) {
continue
}
newTokens.push({ address: addr, symbol, decimals })
} catch {
// not ERC-20, skip
}
}
console.log(newTokens)
if (newTokens.length > 0) {
discoveredTokens = [...discoveredTokens, ...newTokens]
const newBals = await Promise.all(
newTokens.map(async (t) => {
try {
const c = new Contract(t.address, ERC20_ABI, provider)
const bal = await c.balanceOf(address)
return {
symbol: t.symbol,
balance: formatUnits(bal, t.decimals),
address: t.address,
}
} catch {
return { symbol: t.symbol, balance: '0', address: t.address }
}
}),
)
tokenBalances = [...tokenBalances, ...newBals]
}
} }
return { return {
@@ -342,9 +195,6 @@ export const Wallet = () => {
get explorerUrl() { get explorerUrl() {
return net().explorerUrl return net().explorerUrl
}, },
get tokens() {
return net().tokens
},
get loading() { get loading() {
return loading return loading
}, },
@@ -360,21 +210,18 @@ export const Wallet = () => {
get networks() { get networks() {
return networks return networks
}, },
get etherscanKey() { get apiKey() {
return etherscanKey return apiKey
}, },
get transactions() { get transactions() {
return transactions return transactions
}, },
get discoveredTokens() {
return discoveredTokens
},
refresh, refresh,
init, init,
lock, lock,
saveVault, saveVault,
switchNetwork, switchNetwork,
saveEtherscanKey, saveApiKey,
fetchTxHistory, fetchTxHistory,
} }
} }
+75 -45
View File
@@ -10,9 +10,9 @@
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '$lib/components/ui/card/index.js' } from '$lib/components/ui/card/index.js'
import { formatEther, formatUnits } from 'ethers'
const CDN = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains' const CDN =
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains'
const chainDir: Record<string, string> = { const chainDir: Record<string, string> = {
eth: 'ethereum', eth: 'ethereum',
@@ -29,26 +29,14 @@
return `${CDN}/${chainDir[id]}/assets/${address}/logo.png` return `${CDN}/${chainDir[id]}/assets/${address}/logo.png`
} }
const SELECTORS: Record<string, string> = {
'0x': 'Send',
'0xa9059cbb': 'Transfer',
'0x095ea7b3': 'Approve',
'0x23b872dd': 'TransferFrom',
'0x38ed1739': 'Swap',
'0x7ff36ab5': 'Swap',
'0x18cbafe5': 'Swap',
'0x022c0d9f': 'Swap',
'0x3593564c': 'Swap',
'0x49404b7c': 'Swap',
'0x414bf389': 'Swap',
}
function txAction(tx: TxEntry, address: string): string { function txAction(tx: TxEntry, address: string): string {
if (tx.pairedValue) return 'Swap' if (tx.pairedValue) return 'Swap'
const dir = tx.from.toLowerCase() === address.toLowerCase() ? 'Sent' : 'Received' const dir =
tx.from.toLowerCase() === address.toLowerCase()
? 'Sent'
: 'Received'
if (tx.tokenSymbol) return `${dir} ${tx.tokenSymbol}` if (tx.tokenSymbol) return `${dir} ${tx.tokenSymbol}`
const sel = tx.input?.slice(0, 10) ?? '0x' return dir
return SELECTORS[sel] ?? 'Contract Interaction'
} }
let w = Wallet() let w = Wallet()
@@ -132,19 +120,25 @@
src={nativeIcon(w.network)} src={nativeIcon(w.network)}
alt="" alt=""
class="size-4 rounded-full" class="size-4 rounded-full"
onerror={(e) => (e.target as HTMLElement).style.display = 'none'} /> onerror={(e) =>
{w.balance} {w.symbol} ((e.target as HTMLElement).style.display =
'none')} />
{w.balance}
{w.symbol}
</p> </p>
</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"> <p
class="flex items-center gap-1.5 font-mono text-sm">
<img <img
src={tokenIcon(w.network, t.address)} src={t.logo}
alt="" alt=""
class="size-4 rounded-full" class="size-4 rounded-full"
onerror={(e) => (e.target as HTMLElement).style.display = 'none'} /> onerror={(e) =>
((e.target as HTMLElement).style.display =
'none')} />
{Number(t.balance) < 0.0001 && Number(t.balance) > 0 {Number(t.balance) < 0.0001 && Number(t.balance) > 0
? '<0.0001' ? '<0.0001'
: Number(t.balance).toFixed(4)} : Number(t.balance).toFixed(4)}
@@ -174,15 +168,15 @@
<CardTitle>Transactions</CardTitle> <CardTitle>Transactions</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{#if !w.etherscanKey} {#if !w.apiKey}
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<input <input
class="flex h-8 w-full rounded-md border border-input bg-transparent px-2 py-1 font-mono text-xs" class="flex h-8 w-full rounded-md border border-input bg-transparent px-2 py-1 font-mono text-xs"
placeholder="Etherscan API key" placeholder="Alchemy API key"
bind:value={apiKeyInput} /> bind:value={apiKeyInput} />
<Button <Button
size="sm" size="sm"
onclick={() => w.saveEtherscanKey(apiKeyInput)} onclick={() => w.saveApiKey(apiKeyInput)}
disabled={!apiKeyInput.trim()}> disabled={!apiKeyInput.trim()}>
Save Save
</Button> </Button>
@@ -199,24 +193,62 @@
<div class="min-w-0 flex-1 space-y-0.5"> <div class="min-w-0 flex-1 space-y-0.5">
<p> <p>
<button <button
onclick={() => electrobun.rpc?.request.openExternal({ url: w.explorerUrl + tx.hash })} onclick={() =>
electrobun.rpc?.request.openExternal({
url: w.explorerUrl + tx.hash,
})}
class="hover:underline cursor-pointer font-medium"> class="hover:underline cursor-pointer font-medium">
{txAction(tx, w.address)} {txAction(tx, w.address)}
</button> </button>
</p> </p>
<p class="flex items-center gap-1 text-muted-foreground font-mono"> <p
class="flex items-center gap-1 text-muted-foreground font-mono">
{#if tx.pairedValue} {#if tx.pairedValue}
<img src={nativeIcon(w.network)} alt="" class="size-3.5 rounded-full" onerror={(e) => (e.target as HTMLElement).style.display = 'none'} /> <img
{Number(formatEther(tx.value)).toFixed(4)} {w.symbol} src={nativeIcon(w.network)}
alt=""
class="size-3.5 rounded-full"
onerror={(e) =>
((e.target as HTMLElement).style.display =
'none')} />
{Number(tx.value).toFixed(4)}
{w.symbol}
<span class="mx-0.5">→</span> <span class="mx-0.5">→</span>
<img src={tokenIcon(w.network, tx.contractAddress)} alt="" class="size-3.5 rounded-full" onerror={(e) => (e.target as HTMLElement).style.display = 'none'} /> <img
{Number(formatUnits(tx.pairedValue, Number(tx.pairedDecimals ?? 18))).toFixed(4)} {tx.pairedSymbol} src={tokenIcon(
w.network,
tx.contractAddress,
)}
alt=""
class="size-3.5 rounded-full"
onerror={(e) =>
((e.target as HTMLElement).style.display =
'none')} />
{Number(tx.pairedValue).toFixed(4)}
{tx.pairedSymbol}
{:else if tx.tokenSymbol} {:else if tx.tokenSymbol}
<img src={tokenIcon(w.network, tx.contractAddress)} alt="" class="size-3.5 rounded-full" onerror={(e) => (e.target as HTMLElement).style.display = 'none'} /> <img
{Number(formatUnits(tx.value, Number(tx.tokenDecimal ?? 18))).toFixed(4)} {tx.tokenSymbol} src={tokenIcon(
w.network,
tx.contractAddress,
)}
alt=""
class="size-3.5 rounded-full"
onerror={(e) =>
((e.target as HTMLElement).style.display =
'none')} />
{Number(tx.value).toFixed(4)}
{tx.tokenSymbol}
{:else} {:else}
<img src={nativeIcon(w.network)} alt="" class="size-3.5 rounded-full" onerror={(e) => (e.target as HTMLElement).style.display = 'none'} /> <img
{Number(formatEther(tx.value)).toFixed(4)} {w.symbol} src={nativeIcon(w.network)}
alt=""
class="size-3.5 rounded-full"
onerror={(e) =>
((e.target as HTMLElement).style.display =
'none')} />
{Number(tx.value).toFixed(4)}
{w.symbol}
{/if} {/if}
</p> </p>
<p class="text-muted-foreground"> <p class="text-muted-foreground">
@@ -230,20 +262,18 @@
minute: '2-digit', minute: '2-digit',
})} })}
</p> </p>
<p class="text-muted-foreground/50 truncate font-mono"> <p
class="text-muted-foreground/50 truncate font-mono">
<button <button
onclick={() => electrobun.rpc?.request.openExternal({ url: w.explorerUrl + tx.hash })} onclick={() =>
electrobun.rpc?.request.openExternal({
url: w.explorerUrl + tx.hash,
})}
class="hover:underline cursor-pointer"> class="hover:underline cursor-pointer">
{tx.hash.slice(0, 10)}... {tx.hash.slice(0, 10)}...
</button> </button>
</p> </p>
</div> </div>
{#if !tx.tokenSymbol || tx.pairedValue}
<span
class="ml-2 shrink-0 {tx.isError === '0' ? 'text-green-500' : 'text-red-500'}">
{tx.isError === '0' ? '✓' : '✗'}
</span>
{/if}
</div> </div>
{/each} {/each}
</div> </div>