merge transaction action like swap
This commit is contained in:
@@ -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) => ({
|
}
|
||||||
|
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[] = []
|
||||||
|
|
||||||
|
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,
|
hash: t.hash,
|
||||||
timeStamp: t.timeStamp,
|
timeStamp: t.timeStamp,
|
||||||
from: t.from,
|
from: t.from,
|
||||||
to: t.to,
|
to: t.to,
|
||||||
value: t.value,
|
value: t.value,
|
||||||
tokenSymbol: t.tokenSymbol,
|
tokenSymbol: t.tokenSymbol,
|
||||||
tokenName: t.tokenName,
|
|
||||||
tokenDecimal: t.tokenDecimal,
|
tokenDecimal: t.tokenDecimal,
|
||||||
contractAddress: t.contractAddress,
|
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),
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user