From f5ddc8df1b4a8ad61c7b57d5caaa3e86467bf797 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sat, 16 May 2026 14:21:49 +0800 Subject: [PATCH] add transaction view page --- apps/desktop/src/bun/index.ts | 81 ++++++++- .../lib/alchemy/get-transaction-history.ts | 1 - .../bun/lib/viem/get-transaction-details.ts | 50 ++++++ apps/desktop/src/bun/lib/viem/getChain.ts | 12 ++ apps/desktop/src/bun/lib/viem/getClient.ts | 19 ++ apps/desktop/src/bun/lib/viem/index.ts | 2 + apps/desktop/src/lib/electrobun.ts | 23 +++ apps/desktop/src/routes/index.svelte | 38 ++-- .../src/routes/tx/[chainid]/[hash].svelte | 163 ++++++++++++++++++ 9 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 apps/desktop/src/bun/lib/viem/get-transaction-details.ts create mode 100644 apps/desktop/src/bun/lib/viem/getChain.ts create mode 100644 apps/desktop/src/bun/lib/viem/getClient.ts create mode 100644 apps/desktop/src/bun/lib/viem/index.ts create mode 100644 apps/desktop/src/routes/tx/[chainid]/[hash].svelte diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index cdb29ce..c973815 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -16,6 +16,7 @@ import { migrate } from 'drizzle-orm/bun-sqlite/migrator' import { db } from './lib/db' import { join } from 'path' import { getENV } from './lib/get-env' +import { getClient, getTransactionDetails } from './lib/viem' const env = getENV() @@ -88,6 +89,20 @@ type TokenBalanceResult = { logo?: string } +type TxDetailsResult = { + hash: string + from: string + to: string | null + value: string + blockNumber: string | null + fee: string + gasPrice: string + status: 'success' | 'reverted' + type: string + nonce: number + input: string +} + type TxEntry = { hash: string timeStamp: string @@ -97,9 +112,12 @@ type TxEntry = { tokenSymbol?: string tokenDecimal?: string contractAddress?: string + logo?: string pairedValue?: string pairedSymbol?: string pairedDecimals?: string + pairedContractAddress?: string + pairedLogo?: string } type RPC = { @@ -134,6 +152,13 @@ type RPC = { } response: TokenBalanceResult[] } + fetchTransactionDetails: { + params: { + hash: string + chainid: string + } + response: TxDetailsResult | null + } openExternal: { params: { url: string @@ -265,7 +290,14 @@ const rpc = BrowserView.defineRPC({ }) if (!key) return [] - const all = await fetchAlchemyTransfers(key, chainid, address) + const [all, allError] = await tryCatch( + fetchAlchemyTransfers(key, chainid, address), + ) + + if (allError) { + console.log(allError) + return [] + } const addrLower = address.toLowerCase() const byHash = new Map() @@ -312,6 +344,8 @@ const rpc = BrowserView.defineRPC({ pairedDecimals: tokIn[0].rawContract?.decimal ? String(parseInt(tokIn[0].rawContract.decimal, 16)) : undefined, + pairedContractAddress: + tokIn[0].rawContract?.address ?? undefined, }) } else if (extOut.length > 0) { combined.push(mapTransfer(extOut[0])) @@ -325,6 +359,8 @@ const rpc = BrowserView.defineRPC({ pairedDecimals: tokIn[0].rawContract?.decimal ? String(parseInt(tokIn[0].rawContract.decimal, 16)) : undefined, + pairedContractAddress: + tokIn[0].rawContract?.address ?? undefined, }) } else if (tokOut.length > 0) { combined.push(mapTransfer(tokOut[0])) @@ -333,6 +369,34 @@ const rpc = BrowserView.defineRPC({ } } + const addrSet = new Set() + for (const entry of combined) { + if (entry.contractAddress) + addrSet.add(entry.contractAddress.toLowerCase()) + if (entry.pairedContractAddress) + addrSet.add(entry.pairedContractAddress.toLowerCase()) + } + + const logoMap = new Map() + 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) + }), + ) + + for (const entry of combined) { + if (entry.contractAddress) + entry.logo = logoMap.get( + entry.contractAddress.toLowerCase(), + ) + if (entry.pairedContractAddress) + entry.pairedLogo = logoMap.get( + entry.pairedContractAddress.toLowerCase(), + ) + } + combined.sort( (a, b) => Number(b.timeStamp) - Number(a.timeStamp), ) @@ -357,6 +421,21 @@ const rpc = BrowserView.defineRPC({ return [] } }, + fetchTransactionDetails: async ({ hash, chainid }) => { + const key = await Bun.secrets.get({ + service: 'koins', + name: 'alchemy_key', + }) + if (!key) return null + try { + const client = getClient(chainid, key) + const details = await getTransactionDetails(client, hash) + return details + } catch (error) { + console.log(error) + return null + } + }, }, messages: {}, }, diff --git a/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts b/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts index 7eb2f88..ddc524d 100644 --- a/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts +++ b/apps/desktop/src/bun/lib/alchemy/get-transaction-history.ts @@ -1,6 +1,5 @@ import { post } from './call' import { getChainID, getURL } from './network' -import { inspect } from 'util' import type { Transaction } from './types' import { numberToHex } from 'viem' diff --git a/apps/desktop/src/bun/lib/viem/get-transaction-details.ts b/apps/desktop/src/bun/lib/viem/get-transaction-details.ts new file mode 100644 index 0000000..17fe7f5 --- /dev/null +++ b/apps/desktop/src/bun/lib/viem/get-transaction-details.ts @@ -0,0 +1,50 @@ +import { + isHex, + formatEther, + type PublicClient, + formatGwei, + decodeFunctionData, +} from 'viem' + +export type TransactionDetails = { + hash: string + from: string + to: string | null + value: string + blockNumber: string | null + fee: string + gasPrice: string + status: 'success' | 'reverted' + type: string + nonce: number + input: string +} + +export const getTransactionDetails = async ( + client: PublicClient, + hash: string, +): Promise => { + if (!isHex(hash)) return null + + const [tx, receipt] = await Promise.all([ + client.getTransaction({ hash }), + client.getTransactionReceipt({ hash }), + ]) + + return { + hash: tx.hash, + from: tx.from, + to: tx.to, + value: formatEther(tx.value), + blockNumber: receipt.blockNumber?.toString() ?? null, + fee: ( + parseFloat(formatGwei(receipt.gasUsed)) * + parseFloat(formatGwei(tx.gasPrice || BigInt(0))) + ).toString(), + gasPrice: formatGwei(tx.gasPrice || BigInt(0)), + status: receipt.status === 'success' ? 'success' : 'reverted', + type: tx.type, + nonce: tx.nonce, + input: tx.input, + } +} diff --git a/apps/desktop/src/bun/lib/viem/getChain.ts b/apps/desktop/src/bun/lib/viem/getChain.ts new file mode 100644 index 0000000..4b44314 --- /dev/null +++ b/apps/desktop/src/bun/lib/viem/getChain.ts @@ -0,0 +1,12 @@ +import { bsc, mainnet, polygon, type Chain } from 'viem/chains' +import type { ChainID } from '../alchemy/network' + +const chains: Record = { + '1': mainnet, + '56': bsc, + '137': polygon, +} + +export const getChain = (id: ChainID) => { + return chains[id] +} diff --git a/apps/desktop/src/bun/lib/viem/getClient.ts b/apps/desktop/src/bun/lib/viem/getClient.ts new file mode 100644 index 0000000..81bf22b --- /dev/null +++ b/apps/desktop/src/bun/lib/viem/getClient.ts @@ -0,0 +1,19 @@ +import { createPublicClient, http } from 'viem' +import { getChainID, getURL } from '../alchemy/network' +import { getChain } from './getChain' + +export const getClient = (chainId: string, apiKey: string) => { + const id = getChainID(chainId) + + if (!id) { + throw new Error() + } + + const url = getURL({ chainId: id, key: apiKey }) + const publicClient = createPublicClient({ + chain: getChain(id), + transport: http(url), + }) + + return publicClient +} diff --git a/apps/desktop/src/bun/lib/viem/index.ts b/apps/desktop/src/bun/lib/viem/index.ts new file mode 100644 index 0000000..6730f46 --- /dev/null +++ b/apps/desktop/src/bun/lib/viem/index.ts @@ -0,0 +1,2 @@ +export * from './getClient' +export * from './get-transaction-details' diff --git a/apps/desktop/src/lib/electrobun.ts b/apps/desktop/src/lib/electrobun.ts index ffc4888..3ef86f6 100644 --- a/apps/desktop/src/lib/electrobun.ts +++ b/apps/desktop/src/lib/electrobun.ts @@ -9,9 +9,25 @@ export type TxEntry = { tokenSymbol?: string tokenDecimal?: string contractAddress?: string + logo?: string pairedValue?: string pairedSymbol?: string pairedDecimals?: string + pairedLogo?: string +} + +export type TransactionDetails = { + hash: string + from: string + to: string | null + value: string + blockNumber: string | null + fee: string + gasPrice: string + status: 'success' | 'reverted' + type: string + nonce: number + input: string } export type TokenBalanceResult = { @@ -53,6 +69,13 @@ type RPC = { } response: TokenBalanceResult[] } + fetchTransactionDetails: { + params: { + hash: string + chainid: string + } + response: TransactionDetails | null + } openExternal: { params: { url: string diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index 4238fc5..d3cb74c 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -2,6 +2,7 @@ import { electrobun, type TxEntry } from '$lib/electrobun.js' import { Wallet } from '$lib/states/wallet.svelte.js' import { Button } from '$lib/components/ui/button/index.js' + import { navigate } from 'sv-router/generated' import { Textarea } from '$lib/components/ui/textarea/index.js' import { Card, @@ -188,18 +189,18 @@ {:else}
{#each w.transactions as tx} -
+ +

+ {txAction(tx, w.address)}

@@ -215,7 +216,7 @@ {w.symbol} →

- + {tx.hash.slice(0, 10)}...

-
+ {/each} {/if} diff --git a/apps/desktop/src/routes/tx/[chainid]/[hash].svelte b/apps/desktop/src/routes/tx/[chainid]/[hash].svelte new file mode 100644 index 0000000..283b870 --- /dev/null +++ b/apps/desktop/src/routes/tx/[chainid]/[hash].svelte @@ -0,0 +1,163 @@ + + +
+ + + + +
+ Transaction Details + {#if network} + + {network.name} + + {/if} +
+
+ + {#if loading} +

Loading...

+ {:else if error} +

{error}

+ {:else if details} +
+
+

Hash

+

{details.hash}

+
+
+
+

Status

+

+ {details.status} +

+
+
+

Block

+

{details.blockNumber ?? 'N/A'}

+
+
+
+

From

+

{details.from}

+
+
+

To

+

{details.to ?? 'N/A'}

+
+
+

+ Value ({network?.symbol ?? 'ETH'}) +

+

{details.value}

+
+
+
+

+ Tx Fee ({network?.symbol ?? 'ETH'}) +

+

{details.fee}

+
+
+

+ Gas Price (Gwei) +

+

{details.gasPrice}

+
+
+
+
+

Nonce

+

{details.nonce}

+
+
+

Type

+

{details.type}

+
+
+
+

+ Input Data (raw) +

+

+ {details.input} +

+
+
+
+ +
+ {:else} +

+ No details available +

+ {/if} +
+
+