From d2c2a46e54d3b5ed3706c8f0196e113f4a9e3f93 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Mon, 11 May 2026 16:31:48 +0800 Subject: [PATCH] filter out tokens without logo on tokens list --- apps/desktop/src/bun/index.ts | 352 ++++++++++++------- apps/desktop/src/lib/electrobun.ts | 19 +- apps/desktop/src/lib/states/wallet.svelte.ts | 189 +--------- apps/desktop/src/routes/index.svelte | 120 ++++--- 4 files changed, 324 insertions(+), 356 deletions(-) diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index dbe6e16..a9096e7 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -7,6 +7,7 @@ import { type RPCSchema, Utils, } from 'electrobun/bun' +import { inspect } from 'util' const DEV_SERVER_PORT = 5173 const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}` @@ -56,7 +57,18 @@ ApplicationMenu.setApplicationMenu([ const url = await getMainViewUrl() -const ETHERSCAN_API = 'https://api.etherscan.io/v2/api' +const ALCHEMY_NETWORKS: Record = { + '1': 'eth-mainnet', + '137': 'polygon-mainnet', + '56': 'bnb-mainnet', +} + +type TokenBalanceResult = { + symbol: string + balance: string + contractAddress: string + logo?: string +} type TxEntry = { hash: string @@ -64,10 +76,7 @@ type TxEntry = { from: string to: string value: string - input?: string - isError?: string tokenSymbol?: string - tokenName?: string tokenDecimal?: string contractAddress?: string pairedValue?: string @@ -97,11 +106,16 @@ type RPC = { params: { address: string chainid: string - page?: number - offset?: number } response: Promise } + fetchTokenBalances: { + params: { + address: string + chainid: string + } + response: Promise + } openExternal: { params: { url: string @@ -117,32 +131,148 @@ type RPC = { }> } -async function fetchEtherscan( +async function fetchAlchemyTransfers( key: string, chainid: string, - action: string, address: string, - page: number, - offset: number, ): Promise { - const params = new URLSearchParams({ - module: 'account', - action, - address, - page: String(page), - offset: String(offset), - sort: 'desc', - apikey: key, - chainid, + const network = ALCHEMY_NETWORKS[chainid] + if (!network) return [] + const url = `https://${network}.g.alchemy.com/v2/${key}` + + const body = (fromAddress?: string, toAddress?: string) => ({ + jsonrpc: '2.0', + id: 0, + method: 'alchemy_getAssetTransfers', + params: [ + { + ...(fromAddress ? { fromAddress } : {}), + ...(toAddress ? { toAddress } : {}), + category: ['external', 'erc20'], + withMetadata: true, + maxCount: '0x3E8', + }, + ], }) - const res = await fetch(`${ETHERSCAN_API}?${params}`) - const data = await res.json() - if (data.status !== '1') return [] - return data.result + + const [outRes, inRes] = await Promise.all([ + fetch(url, { + 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() + 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 { + 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({ - maxRequestTime: 20000, + maxRequestTime: 200000, handlers: { requests: { getSecret: async ({ name, service }) => { @@ -154,140 +284,78 @@ const rpc = BrowserView.defineRPC({ openExternal: async ({ url }) => { Utils.openExternal(url) }, - fetchTxHistory: async ({ - address, - chainid, - page = 1, - offset = 1000, - }) => { + fetchTxHistory: async ({ address, chainid }) => { const key = await Bun.secrets.get({ service: 'koins', - name: 'etherscan_key', + name: 'alchemy_key', }) if (!key) return [] - const [nativeTxs, tokenTxs] = await Promise.all([ - fetchEtherscan( - key, - chainid, - 'txlist', - address, - page, - offset, - ), - fetchEtherscan( - key, - chainid, - 'tokentx', - address, - page, - offset, - ), - ]) + const all = await fetchAlchemyTransfers(key, chainid, address) const addrLower = address.toLowerCase() - const byHash = new Map< - string, - { native?: any; token: any[] } - >() - for (const t of nativeTxs) { + const byHash = new Map() + for (const t of all) { const h = t.hash - if (!byHash.has(h)) byHash.set(h, { token: [] }) - byHash.get(h)!.native = t - } - for (const t of tokenTxs) { - const h = t.hash - if (!byHash.has(h)) byHash.set(h, { token: [] }) - byHash.get(h)!.token.push(t) + if (!byHash.has(h)) byHash.set(h, []) + byHash.get(h)!.push(t) } const combined: TxEntry[] = [] for (const [, group] of byHash) { - console.log(group) - const { native, token } = group - const nv = native?.value ?? '0' + const external = group.filter( + (t: any) => t.category === 'external', + ) + 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, ) - const outgoing = token.filter( + const tokIn = tokens.filter( + (t: any) => t.to.toLowerCase() === addrLower, + ) + const tokOut = tokens.filter( (t: any) => t.from.toLowerCase() === addrLower, ) - if (native && nv !== '0' && incoming.length > 0) { + if (extOut.length > 0 && tokIn.length > 0) { combined.push({ - hash: native.hash, - timeStamp: native.timeStamp, - from: native.from, - to: native.to, - value: native.value, - input: native.input, - isError: native.isError, - tokenSymbol: incoming[0].tokenSymbol, - tokenDecimal: incoming[0].tokenDecimal, - contractAddress: incoming[0].contractAddress, - pairedValue: incoming[0].value, - pairedSymbol: incoming[0].tokenSymbol, - pairedDecimals: incoming[0].tokenDecimal, + ...mapTransfer(extOut[0]), + tokenSymbol: tokIn[0].asset, + tokenDecimal: tokIn[0].rawContract?.decimal + ? String(parseInt(tokIn[0].rawContract.decimal, 16)) + : undefined, + contractAddress: + tokIn[0].rawContract?.address ?? undefined, + pairedValue: String(tokIn[0].value), + pairedSymbol: tokIn[0].asset, + pairedDecimals: tokIn[0].rawContract?.decimal + ? String(parseInt(tokIn[0].rawContract.decimal, 16)) + : undefined, }) - } 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({ - hash: native.hash, - timeStamp: native.timeStamp, - from: native.from, - to: native.to, - value: native.value, - input: native.input, - 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, + ...mapTransfer(tokOut[0]), + pairedValue: String(tokIn[0].value), + pairedSymbol: tokIn[0].asset, + pairedDecimals: tokIn[0].rawContract?.decimal + ? String(parseInt(tokIn[0].rawContract.decimal, 16)) + : undefined, }) + } 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({ 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: {}, }, diff --git a/apps/desktop/src/lib/electrobun.ts b/apps/desktop/src/lib/electrobun.ts index ac3930f..9d95225 100644 --- a/apps/desktop/src/lib/electrobun.ts +++ b/apps/desktop/src/lib/electrobun.ts @@ -6,10 +6,7 @@ export type TxEntry = { from: string to: string value: string - input?: string - isError?: string tokenSymbol?: string - tokenName?: string tokenDecimal?: string contractAddress?: string pairedValue?: string @@ -17,6 +14,13 @@ export type TxEntry = { pairedDecimals?: string } +export type TokenBalanceResult = { + symbol: string + balance: string + contractAddress: string + logo?: string +} + type RPC = { bun: { requests: { @@ -39,11 +43,16 @@ type RPC = { params: { address: string chainid: string - page?: number - offset?: number } response: Promise } + fetchTokenBalances: { + params: { + address: string + chainid: string + } + response: Promise + } openExternal: { params: { url: string diff --git a/apps/desktop/src/lib/states/wallet.svelte.ts b/apps/desktop/src/lib/states/wallet.svelte.ts index 0191d51..10ac106 100644 --- a/apps/desktop/src/lib/states/wallet.svelte.ts +++ b/apps/desktop/src/lib/states/wallet.svelte.ts @@ -2,23 +2,9 @@ import { JsonRpcProvider, HDNodeWallet, formatEther, - formatUnits, - Contract, } from 'ethers' 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 = [ { id: 'bsc', @@ -27,18 +13,6 @@ export const networks = [ symbol: 'BNB', chainid: '56', explorerUrl: 'https://bscscan.com/tx/', - tokens: [ - { - symbol: 'USDC', - address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', - decimals: 18, - }, - { - symbol: 'USDT', - address: '0x55d398326f99059fF775485246999027B3197955', - decimals: 18, - }, - ], }, { id: 'polygon', @@ -47,18 +21,6 @@ export const networks = [ symbol: 'POL', chainid: '137', explorerUrl: 'https://polygonscan.com/tx/', - tokens: [ - { - symbol: 'USDC', - address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', - decimals: 6, - }, - { - symbol: 'USDT', - address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', - decimals: 6, - }, - ], }, { id: 'eth', @@ -67,23 +29,6 @@ export const networks = [ symbol: 'ETH', chainid: '1', 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 @@ -92,7 +37,8 @@ export type NetworkId = (typeof networks)[number]['id'] export type TokenBalance = { symbol: string balance: string - address?: string + contractAddress: string + logo?: string } export const Wallet = () => { @@ -105,9 +51,8 @@ export const Wallet = () => { let error = $state('') let vaultExists = $state(false) let ready = $state(false) - let etherscanKey = $state('') + let apiKey = $state('') let transactions = $state([]) - let discoveredTokens = $state([]) const net = () => networks.find((n) => n.id === network)! @@ -120,9 +65,9 @@ export const Wallet = () => { vaultExists = raw !== null && raw !== undefined const key = await electrobun.rpc?.request.getSecret({ service: 'koins', - name: 'etherscan_key', + name: 'alchemy_key', }) - if (key) etherscanKey = key + if (key) apiKey = key if (raw) { seed = raw await refresh() @@ -138,7 +83,6 @@ export const Wallet = () => { const switchNetwork = async (id: NetworkId) => { network = id - discoveredTokens = [] if (seed) await refresh() } @@ -169,7 +113,6 @@ export const Wallet = () => { error = '' tokenBalances = [] transactions = [] - discoveredTokens = [] } const refresh = async () => { @@ -184,39 +127,12 @@ export const Wallet = () => { const nativeBal = await provider.getBalance(wallet.address) balance = formatEther(nativeBal) - const [hardcodedBals, discoveredBals] = await Promise.all([ - Promise.all( - net().tokens.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 } - } - }), - ), - 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] + const bals = await electrobun.rpc?.request.fetchTokenBalances({ + address: wallet.address, + chainid: net().chainid, + }) + tokenBalances = bals ?? [] + fetchTxHistory() } catch (e) { error = @@ -226,18 +142,18 @@ export const Wallet = () => { } } - const saveEtherscanKey = async (key: string) => { + const saveApiKey = async (key: string) => { await electrobun.rpc?.request.setSecret({ service: 'koins', - name: 'etherscan_key', + name: 'alchemy_key', value: key, }) - etherscanKey = key + apiKey = key if (address) await fetchTxHistory() } const fetchTxHistory = async () => { - if (!address || !etherscanKey) { + if (!address || !apiKey) { transactions = [] return } @@ -246,69 +162,6 @@ export const Wallet = () => { chainid: net().chainid, }) 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 { @@ -342,9 +195,6 @@ export const Wallet = () => { get explorerUrl() { return net().explorerUrl }, - get tokens() { - return net().tokens - }, get loading() { return loading }, @@ -360,21 +210,18 @@ export const Wallet = () => { get networks() { return networks }, - get etherscanKey() { - return etherscanKey + get apiKey() { + return apiKey }, get transactions() { return transactions }, - get discoveredTokens() { - return discoveredTokens - }, refresh, init, lock, saveVault, switchNetwork, - saveEtherscanKey, + saveApiKey, fetchTxHistory, } } diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index 9732dec..4238fc5 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -10,9 +10,9 @@ CardHeader, CardTitle, } 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 = { eth: 'ethereum', @@ -29,26 +29,14 @@ return `${CDN}/${chainDir[id]}/assets/${address}/logo.png` } - const SELECTORS: Record = { - '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 { 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}` - const sel = tx.input?.slice(0, 10) ?? '0x' - return SELECTORS[sel] ?? 'Contract Interaction' + return dir } let w = Wallet() @@ -132,19 +120,25 @@ src={nativeIcon(w.network)} alt="" class="size-4 rounded-full" - onerror={(e) => (e.target as HTMLElement).style.display = 'none'} /> - {w.balance} {w.symbol} + onerror={(e) => + ((e.target as HTMLElement).style.display = + 'none')} /> + {w.balance} + {w.symbol}

Tokens

{#each w.tokenBalances as t} -

+

(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 ? '<0.0001' : Number(t.balance).toFixed(4)} @@ -174,15 +168,15 @@ Transactions - {#if !w.etherscanKey} + {#if !w.apiKey}

@@ -199,24 +193,62 @@

-

+

{#if tx.pairedValue} - (e.target as HTMLElement).style.display = 'none'} /> - {Number(formatEther(tx.value)).toFixed(4)} {w.symbol} + + ((e.target as HTMLElement).style.display = + 'none')} /> + {Number(tx.value).toFixed(4)} + {w.symbol} → - (e.target as HTMLElement).style.display = 'none'} /> - {Number(formatUnits(tx.pairedValue, Number(tx.pairedDecimals ?? 18))).toFixed(4)} {tx.pairedSymbol} + + ((e.target as HTMLElement).style.display = + 'none')} /> + {Number(tx.pairedValue).toFixed(4)} + {tx.pairedSymbol} {:else if tx.tokenSymbol} - (e.target as HTMLElement).style.display = 'none'} /> - {Number(formatUnits(tx.value, Number(tx.tokenDecimal ?? 18))).toFixed(4)} {tx.tokenSymbol} + + ((e.target as HTMLElement).style.display = + 'none')} /> + {Number(tx.value).toFixed(4)} + {tx.tokenSymbol} {:else} - (e.target as HTMLElement).style.display = 'none'} /> - {Number(formatEther(tx.value)).toFixed(4)} {w.symbol} + + ((e.target as HTMLElement).style.display = + 'none')} /> + {Number(tx.value).toFixed(4)} + {w.symbol} {/if}

@@ -230,20 +262,18 @@ minute: '2-digit', })}

-

+

- {#if !tx.tokenSymbol || tx.pairedValue} - - {tx.isError === '0' ? '✓' : '✗'} - - {/if}
{/each}