From 0074d6fb4ef68f9032742e197648cd89c6da74d3 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Mon, 11 May 2026 11:27:24 +0800 Subject: [PATCH] add native token + internal transfers --- apps/desktop/src/bun/index.ts | 127 ++++++++++++++----- apps/desktop/src/lib/electrobun.ts | 25 ++-- apps/desktop/src/lib/states/wallet.svelte.ts | 94 +++++++++++++- apps/desktop/src/routes/index.svelte | 91 +++++++++---- 4 files changed, 267 insertions(+), 70 deletions(-) diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index c0999ed..134ae69 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -5,6 +5,7 @@ import { Updater, defineElectrobunRPC, type RPCSchema, + Utils, } from 'electrobun/bun' const DEV_SERVER_PORT = 5173 @@ -57,20 +58,17 @@ const url = await getMainViewUrl() const ETHERSCAN_API = 'https://api.etherscan.io/v2/api' -type EtherscanTx = { - blockNumber: string - timeStamp: string +type TxEntry = { hash: string + timeStamp: string from: string to: string value: string - gas: string - gasPrice: string - isError: string - txreceipt_status: string - input: string - contractAddress: string - gasUsed: string + isError?: string + tokenSymbol?: string + tokenName?: string + tokenDecimal?: string + contractAddress?: string } type RPC = { @@ -98,7 +96,13 @@ type RPC = { page?: number offset?: number } - response: Promise + response: Promise + } + openExternal: { + params: { + url: string + } + response: Promise } } messages: {} @@ -108,8 +112,34 @@ type RPC = { messages: {} }> } + +async function fetchEtherscan( + 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 res = await fetch(`${ETHERSCAN_API}?${params}`) + const data = await res.json() + console.log(data) + if (data.status !== '1') return [] + return data.result +} + const rpc = BrowserView.defineRPC({ - maxRequestTime: 10000, + maxRequestTime: 20000, handlers: { requests: { getSecret: async ({ name, service }) => { @@ -118,38 +148,67 @@ const rpc = BrowserView.defineRPC({ setSecret: async ({ name, service, value }) => { await Bun.secrets.set({ name, service, value }) }, + openExternal: async ({ url }) => { + Utils.openExternal(url) + }, fetchTxHistory: async ({ address, chainid, page = 1, - offset = 20, + offset = 1000, }) => { const key = await Bun.secrets.get({ service: 'koins', name: 'etherscan_key', }) if (!key) return [] - const params = new URLSearchParams({ - module: 'account', - action: 'txlist', - address, - page: String(page), - offset: String(offset), - sort: 'desc', - apikey: key, - chainid, - }) - try { - const url = `${ETHERSCAN_API}?${params}` - const res = await fetch(url) - const data = await res.json() - console.log(data) - if (data.status !== '1') return [] - return data.result as EtherscanTx[] - } catch (error) { - console.log(error) - return [] - } + + const [nativeTxs, tokenTxs] = await Promise.all([ + fetchEtherscan( + key, + chainid, + 'txlist', + address, + page, + offset, + ), + fetchEtherscan( + key, + chainid, + 'tokentx', + address, + page, + offset, + ), + ]) + + const combined: TxEntry[] = [ + ...nativeTxs.map((t: any) => ({ + hash: t.hash, + timeStamp: t.timeStamp, + from: t.from, + to: t.to, + value: t.value, + isError: t.isError, + })), + ...tokenTxs.map((t: any) => ({ + hash: t.hash, + timeStamp: t.timeStamp, + from: t.from, + to: t.to, + value: t.value, + tokenSymbol: t.tokenSymbol, + tokenName: t.tokenName, + tokenDecimal: t.tokenDecimal, + contractAddress: t.contractAddress, + })), + ] + + combined.sort( + (a, b) => Number(b.timeStamp) - Number(a.timeStamp), + ) + + return combined }, }, messages: {}, diff --git a/apps/desktop/src/lib/electrobun.ts b/apps/desktop/src/lib/electrobun.ts index f8166b4..5ef6e2f 100644 --- a/apps/desktop/src/lib/electrobun.ts +++ b/apps/desktop/src/lib/electrobun.ts @@ -1,19 +1,16 @@ import { Electroview } from 'electrobun/view' -export type EtherscanTx = { - blockNumber: string - timeStamp: string +export type TxEntry = { hash: string + timeStamp: string from: string to: string value: string - isError: string - txreceipt_status: string - gas: string - gasPrice: string - input: string - contractAddress: string - gasUsed: string + isError?: string + tokenSymbol?: string + tokenName?: string + tokenDecimal?: string + contractAddress?: string } type RPC = { @@ -41,7 +38,13 @@ type RPC = { page?: number offset?: number } - response: Promise + response: Promise + } + openExternal: { + params: { + url: string + } + response: Promise } } messages: {} diff --git a/apps/desktop/src/lib/states/wallet.svelte.ts b/apps/desktop/src/lib/states/wallet.svelte.ts index 62d5dcd..22ff2e7 100644 --- a/apps/desktop/src/lib/states/wallet.svelte.ts +++ b/apps/desktop/src/lib/states/wallet.svelte.ts @@ -5,12 +5,20 @@ import { formatUnits, Contract, } from 'ethers' -import { electrobun, type EtherscanTx } 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 = [ { id: 'bsc', @@ -35,7 +43,7 @@ export const networks = [ { id: 'polygon', name: 'Polygon', - rpc: 'https://polygon.drpc.org', + rpc: 'https://polygon-bor.publicnode.com', symbol: 'POL', chainid: '137', explorerUrl: 'https://polygonscan.com/tx/', @@ -94,7 +102,8 @@ export const Wallet = () => { let vaultExists = $state(false) let ready = $state(false) let etherscanKey = $state('') - let transactions = $state([]) + let transactions = $state([]) + let discoveredTokens = $state([]) const net = () => networks.find((n) => n.id === network)! @@ -125,6 +134,7 @@ export const Wallet = () => { const switchNetwork = async (id: NetworkId) => { network = id + discoveredTokens = [] if (seed) await refresh() } @@ -155,6 +165,7 @@ export const Wallet = () => { error = '' tokenBalances = [] transactions = [] + discoveredTokens = [] } const refresh = async () => { @@ -180,6 +191,18 @@ export const Wallet = () => { return { symbol: t.symbol, balance: '0' } } }), + ...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), + } + } catch { + return { symbol: t.symbol, balance: '0' } + } + }), ]) balance = formatEther(nativeBal) tokenBalances = tokenBals @@ -212,6 +235,68 @@ 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), + } + } catch { + return { symbol: t.symbol, balance: '0' } + } + }), + ) + tokenBalances = [...tokenBalances, ...newBals] + } } return { @@ -269,6 +354,9 @@ export const Wallet = () => { get transactions() { return transactions }, + get discoveredTokens() { + return discoveredTokens + }, refresh, init, lock, diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index bca1aaf..cbdac3c 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -1,4 +1,5 @@