merge transaction action like swap

This commit is contained in:
2026-05-11 12:20:21 +08:00
parent 0074d6fb4e
commit 5d15f1a91f
4 changed files with 168 additions and 67 deletions
+89 -21
View File
@@ -64,11 +64,15 @@ type TxEntry = {
from: string from: string
to: string to: string
value: string value: string
input?: string
isError?: string isError?: string
tokenSymbol?: string tokenSymbol?: string
tokenName?: string tokenName?: string
tokenDecimal?: string tokenDecimal?: string
contractAddress?: string contractAddress?: string
pairedValue?: string
pairedSymbol?: string
pairedDecimals?: string
} }
type RPC = { type RPC = {
@@ -182,27 +186,91 @@ const rpc = BrowserView.defineRPC<RPC>({
), ),
]) ])
const combined: TxEntry[] = [ const addrLower = address.toLowerCase()
...nativeTxs.map((t: any) => ({ const byHash = new Map<
hash: t.hash, string,
timeStamp: t.timeStamp, { native?: any; token: any[] }
from: t.from, >()
to: t.to, for (const t of nativeTxs) {
value: t.value, const h = t.hash
isError: t.isError, if (!byHash.has(h)) byHash.set(h, { token: [] })
})), byHash.get(h)!.native = t
...tokenTxs.map((t: any) => ({ }
hash: t.hash, for (const t of tokenTxs) {
timeStamp: t.timeStamp, const h = t.hash
from: t.from, if (!byHash.has(h)) byHash.set(h, { token: [] })
to: t.to, byHash.get(h)!.token.push(t)
value: t.value, }
tokenSymbol: t.tokenSymbol,
tokenName: t.tokenName, const combined: TxEntry[] = []
tokenDecimal: t.tokenDecimal,
contractAddress: t.contractAddress, for (const [, group] of byHash) {
})), const { native, token } = group
]
const incoming = token.filter(
(t: any) => t.to.toLowerCase() === addrLower,
)
const outgoing = token.filter(
(t: any) => t.from.toLowerCase() === addrLower,
)
if (native && incoming.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,
})
} 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 (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 {
for (const t of token) {
const isIn = t.to.toLowerCase() === addrLower
combined.push({
hash: t.hash,
timeStamp: t.timeStamp,
from: t.from,
to: t.to,
value: t.value,
tokenSymbol: t.tokenSymbol,
tokenDecimal: t.tokenDecimal,
contractAddress: t.contractAddress,
...(isIn ? {} : { isError: '0' }),
})
}
}
}
combined.sort( combined.sort(
(a, b) => Number(b.timeStamp) - Number(a.timeStamp), (a, b) => Number(b.timeStamp) - Number(a.timeStamp),
+4
View File
@@ -6,11 +6,15 @@ export type TxEntry = {
from: string from: string
to: string to: string
value: string value: string
input?: string
isError?: string isError?: string
tokenSymbol?: string tokenSymbol?: string
tokenName?: string tokenName?: string
tokenDecimal?: string tokenDecimal?: string
contractAddress?: string contractAddress?: string
pairedValue?: string
pairedSymbol?: string
pairedDecimals?: string
} }
type RPC = { type RPC = {
+33 -28
View File
@@ -177,35 +177,40 @@ export const Wallet = () => {
const wallet = HDNodeWallet.fromPhrase(seed) const wallet = HDNodeWallet.fromPhrase(seed)
address = wallet.address address = wallet.address
const provider = new JsonRpcProvider(net().rpc) const provider = new JsonRpcProvider(net().rpc)
const [nativeBal, ...tokenBals] = await Promise.all([ const nativeBal = await provider.getBalance(wallet.address)
provider.getBalance(wallet.address),
...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),
}
} catch {
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) balance = formatEther(nativeBal)
tokenBalances = tokenBals
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),
}
} catch {
return { symbol: t.symbol, balance: '0' }
}
}),
),
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),
}
} catch {
return { symbol: t.symbol, balance: '0' }
}
}),
),
])
tokenBalances = [...hardcodedBals, ...discoveredBals]
fetchTxHistory() fetchTxHistory()
} catch (e) { } catch (e) {
error = error =
+42 -18
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { electrobun } from '$lib/electrobun.js' import { electrobun, type TxEntry } from '$lib/electrobun.js'
import { Wallet } from '$lib/states/wallet.svelte.js' import { Wallet } from '$lib/states/wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { Textarea } from '$lib/components/ui/textarea/index.js' import { Textarea } from '$lib/components/ui/textarea/index.js'
@@ -12,6 +12,28 @@
} from '$lib/components/ui/card/index.js' } from '$lib/components/ui/card/index.js'
import { formatEther, formatUnits } from 'ethers' import { formatEther, formatUnits } from 'ethers'
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 {
if (tx.pairedValue) return 'Swap'
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'
}
let w = Wallet() let w = Wallet()
let inputSeed = $state('') let inputSeed = $state('')
let apiKeyInput = $state('') let apiKeyInput = $state('')
@@ -146,25 +168,22 @@
<div <div
class="flex items-center justify-between rounded-md bg-muted px-3 py-2 text-xs"> class="flex items-center justify-between rounded-md bg-muted px-3 py-2 text-xs">
<div class="min-w-0 flex-1 space-y-0.5"> <div class="min-w-0 flex-1 space-y-0.5">
<p class="truncate font-mono" title={tx.hash}> <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"> class="hover:underline cursor-pointer font-medium">
{tx.hash.slice(0, 10)}... {txAction(tx, w.address)}
</button> </button>
</p> </p>
<p class="text-muted-foreground font-mono"> <p class="text-muted-foreground font-mono">
{#if tx.tokenSymbol} {#if tx.pairedValue}
{Number( {Number(formatEther(tx.value)).toFixed(4)} {w.symbol}
formatUnits( <span class="mx-1">→</span>
tx.value, {Number(formatUnits(tx.pairedValue, Number(tx.pairedDecimals ?? 18))).toFixed(4)} {tx.pairedSymbol}
Number(tx.tokenDecimal ?? 18), {:else if tx.tokenSymbol}
), {Number(formatUnits(tx.value, Number(tx.tokenDecimal ?? 18))).toFixed(4)} {tx.tokenSymbol}
).toFixed(4)}
{tx.tokenSymbol}
{:else} {:else}
{Number(formatEther(tx.value)).toFixed(4)} {Number(formatEther(tx.value)).toFixed(4)} {w.symbol}
{w.symbol}
{/if} {/if}
</p> </p>
<p class="text-muted-foreground"> <p class="text-muted-foreground">
@@ -178,12 +197,17 @@
minute: '2-digit', minute: '2-digit',
})} })}
</p> </p>
<p class="text-muted-foreground/50 truncate font-mono">
<button
onclick={() => electrobun.rpc?.request.openExternal({ url: w.explorerUrl + tx.hash })}
class="hover:underline cursor-pointer">
{tx.hash.slice(0, 10)}...
</button>
</p>
</div> </div>
{#if !tx.tokenSymbol} {#if !tx.tokenSymbol || tx.pairedValue}
<span <span
class="ml-2 shrink-0 {tx.isError === '0' class="ml-2 shrink-0 {tx.isError === '0' ? 'text-green-500' : 'text-red-500'}">
? 'text-green-500'
: 'text-red-500'}">
{tx.isError === '0' ? '✓' : '✗'} {tx.isError === '0' ? '✓' : '✗'}
</span> </span>
{/if} {/if}