add transaction view page
This commit is contained in:
@@ -16,6 +16,7 @@ import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
|||||||
import { db } from './lib/db'
|
import { db } from './lib/db'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { getENV } from './lib/get-env'
|
import { getENV } from './lib/get-env'
|
||||||
|
import { getClient, getTransactionDetails } from './lib/viem'
|
||||||
|
|
||||||
const env = getENV()
|
const env = getENV()
|
||||||
|
|
||||||
@@ -88,6 +89,20 @@ type TokenBalanceResult = {
|
|||||||
logo?: string
|
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 = {
|
type TxEntry = {
|
||||||
hash: string
|
hash: string
|
||||||
timeStamp: string
|
timeStamp: string
|
||||||
@@ -97,9 +112,12 @@ type TxEntry = {
|
|||||||
tokenSymbol?: string
|
tokenSymbol?: string
|
||||||
tokenDecimal?: string
|
tokenDecimal?: string
|
||||||
contractAddress?: string
|
contractAddress?: string
|
||||||
|
logo?: string
|
||||||
pairedValue?: string
|
pairedValue?: string
|
||||||
pairedSymbol?: string
|
pairedSymbol?: string
|
||||||
pairedDecimals?: string
|
pairedDecimals?: string
|
||||||
|
pairedContractAddress?: string
|
||||||
|
pairedLogo?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RPC = {
|
type RPC = {
|
||||||
@@ -134,6 +152,13 @@ type RPC = {
|
|||||||
}
|
}
|
||||||
response: TokenBalanceResult[]
|
response: TokenBalanceResult[]
|
||||||
}
|
}
|
||||||
|
fetchTransactionDetails: {
|
||||||
|
params: {
|
||||||
|
hash: string
|
||||||
|
chainid: string
|
||||||
|
}
|
||||||
|
response: TxDetailsResult | null
|
||||||
|
}
|
||||||
openExternal: {
|
openExternal: {
|
||||||
params: {
|
params: {
|
||||||
url: string
|
url: string
|
||||||
@@ -265,7 +290,14 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
})
|
})
|
||||||
if (!key) return []
|
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 addrLower = address.toLowerCase()
|
||||||
const byHash = new Map<string, any[]>()
|
const byHash = new Map<string, any[]>()
|
||||||
@@ -312,6 +344,8 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
pairedDecimals: tokIn[0].rawContract?.decimal
|
pairedDecimals: tokIn[0].rawContract?.decimal
|
||||||
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
pairedContractAddress:
|
||||||
|
tokIn[0].rawContract?.address ?? undefined,
|
||||||
})
|
})
|
||||||
} else if (extOut.length > 0) {
|
} else if (extOut.length > 0) {
|
||||||
combined.push(mapTransfer(extOut[0]))
|
combined.push(mapTransfer(extOut[0]))
|
||||||
@@ -325,6 +359,8 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
pairedDecimals: tokIn[0].rawContract?.decimal
|
pairedDecimals: tokIn[0].rawContract?.decimal
|
||||||
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
pairedContractAddress:
|
||||||
|
tokIn[0].rawContract?.address ?? undefined,
|
||||||
})
|
})
|
||||||
} else if (tokOut.length > 0) {
|
} else if (tokOut.length > 0) {
|
||||||
combined.push(mapTransfer(tokOut[0]))
|
combined.push(mapTransfer(tokOut[0]))
|
||||||
@@ -333,6 +369,34 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addrSet = new Set<string>()
|
||||||
|
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<string, string>()
|
||||||
|
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(
|
combined.sort(
|
||||||
(a, b) => Number(b.timeStamp) - Number(a.timeStamp),
|
(a, b) => Number(b.timeStamp) - Number(a.timeStamp),
|
||||||
)
|
)
|
||||||
@@ -357,6 +421,21 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
return []
|
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: {},
|
messages: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { post } from './call'
|
import { post } from './call'
|
||||||
import { getChainID, getURL } from './network'
|
import { getChainID, getURL } from './network'
|
||||||
import { inspect } from 'util'
|
|
||||||
import type { Transaction } from './types'
|
import type { Transaction } from './types'
|
||||||
import { numberToHex } from 'viem'
|
import { numberToHex } from 'viem'
|
||||||
|
|
||||||
|
|||||||
@@ -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<TransactionDetails | null> => {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { bsc, mainnet, polygon, type Chain } from 'viem/chains'
|
||||||
|
import type { ChainID } from '../alchemy/network'
|
||||||
|
|
||||||
|
const chains: Record<ChainID, Chain> = {
|
||||||
|
'1': mainnet,
|
||||||
|
'56': bsc,
|
||||||
|
'137': polygon,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChain = (id: ChainID) => {
|
||||||
|
return chains[id]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './getClient'
|
||||||
|
export * from './get-transaction-details'
|
||||||
@@ -9,9 +9,25 @@ export type TxEntry = {
|
|||||||
tokenSymbol?: string
|
tokenSymbol?: string
|
||||||
tokenDecimal?: string
|
tokenDecimal?: string
|
||||||
contractAddress?: string
|
contractAddress?: string
|
||||||
|
logo?: string
|
||||||
pairedValue?: string
|
pairedValue?: string
|
||||||
pairedSymbol?: string
|
pairedSymbol?: string
|
||||||
pairedDecimals?: 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 = {
|
export type TokenBalanceResult = {
|
||||||
@@ -53,6 +69,13 @@ type RPC = {
|
|||||||
}
|
}
|
||||||
response: TokenBalanceResult[]
|
response: TokenBalanceResult[]
|
||||||
}
|
}
|
||||||
|
fetchTransactionDetails: {
|
||||||
|
params: {
|
||||||
|
hash: string
|
||||||
|
chainid: string
|
||||||
|
}
|
||||||
|
response: TransactionDetails | null
|
||||||
|
}
|
||||||
openExternal: {
|
openExternal: {
|
||||||
params: {
|
params: {
|
||||||
url: string
|
url: string
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { electrobun, type TxEntry } 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 { navigate } from 'sv-router/generated'
|
||||||
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -188,18 +189,18 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="max-h-96 space-y-1 overflow-y-auto">
|
<div class="max-h-96 space-y-1 overflow-y-auto">
|
||||||
{#each w.transactions as tx}
|
{#each w.transactions as tx}
|
||||||
<div
|
|
||||||
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">
|
|
||||||
<p>
|
|
||||||
<button
|
<button
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
electrobun.rpc?.request.openExternal({
|
navigate('/tx/:chainid/:hash', {
|
||||||
url: w.explorerUrl + tx.hash,
|
params: {
|
||||||
|
chainid: w.chainid,
|
||||||
|
hash: tx.hash,
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
class="hover:underline cursor-pointer font-medium">
|
class="flex w-full cursor-pointer items-center justify-between rounded-md bg-muted px-3 py-2 text-xs text-left hover:bg-muted/80 transition-colors">
|
||||||
|
<div class="min-w-0 flex-1 space-y-0.5">
|
||||||
|
<p class="font-medium">
|
||||||
{txAction(tx, w.address)}
|
{txAction(tx, w.address)}
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
class="flex items-center gap-1 text-muted-foreground font-mono">
|
class="flex items-center gap-1 text-muted-foreground font-mono">
|
||||||
@@ -215,7 +216,7 @@
|
|||||||
{w.symbol}
|
{w.symbol}
|
||||||
<span class="mx-0.5">→</span>
|
<span class="mx-0.5">→</span>
|
||||||
<img
|
<img
|
||||||
src={tokenIcon(
|
src={tx.pairedLogo || tokenIcon(
|
||||||
w.network,
|
w.network,
|
||||||
tx.contractAddress,
|
tx.contractAddress,
|
||||||
)}
|
)}
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
{tx.pairedSymbol}
|
{tx.pairedSymbol}
|
||||||
{:else if tx.tokenSymbol}
|
{:else if tx.tokenSymbol}
|
||||||
<img
|
<img
|
||||||
src={tokenIcon(
|
src={tx.logo || tokenIcon(
|
||||||
w.network,
|
w.network,
|
||||||
tx.contractAddress,
|
tx.contractAddress,
|
||||||
)}
|
)}
|
||||||
@@ -264,17 +265,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
class="text-muted-foreground/50 truncate font-mono">
|
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)}...
|
{tx.hash.slice(0, 10)}...
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
electrobun,
|
||||||
|
type TransactionDetails,
|
||||||
|
} from '$lib/electrobun.js'
|
||||||
|
import { networks } from '$lib/states/wallet.svelte.js'
|
||||||
|
import { navigate, route } from 'sv-router/generated'
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '$lib/components/ui/card/index.js'
|
||||||
|
|
||||||
|
let details = $state<TransactionDetails | null>(null)
|
||||||
|
let loading = $state(true)
|
||||||
|
let error = $state('')
|
||||||
|
|
||||||
|
const chainid = $derived(route.params.chainid)
|
||||||
|
const hash = $derived(route.params.hash)
|
||||||
|
|
||||||
|
const network = $derived(
|
||||||
|
networks.find((n) => n.chainid === chainid),
|
||||||
|
)
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!hash || !chainid) return
|
||||||
|
loading = true
|
||||||
|
error = ''
|
||||||
|
electrobun.rpc?.request
|
||||||
|
.fetchTransactionDetails({ hash, chainid })
|
||||||
|
.then((res) => {
|
||||||
|
details = res
|
||||||
|
loading = false
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
error =
|
||||||
|
e instanceof Error ? e.message : 'Failed to fetch details'
|
||||||
|
loading = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function explorerUrl() {
|
||||||
|
if (!network) return ''
|
||||||
|
return network.explorerUrl + hash
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto mt-16 max-w-md">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => navigate('/')}
|
||||||
|
class="mb-4">
|
||||||
|
← Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<CardTitle>Transaction Details</CardTitle>
|
||||||
|
{#if network}
|
||||||
|
<span
|
||||||
|
class="rounded-md bg-muted px-2 py-0.5 text-xs font-medium">
|
||||||
|
{network.name}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if loading}
|
||||||
|
<p class="text-muted-foreground text-xs">Loading...</p>
|
||||||
|
{:else if error}
|
||||||
|
<p class="text-red-500 text-xs">{error}</p>
|
||||||
|
{:else if details}
|
||||||
|
<div class="space-y-2 font-mono text-xs">
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">Hash</p>
|
||||||
|
<p class="break-all">{details.hash}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">Status</p>
|
||||||
|
<p
|
||||||
|
class={details.status === 'success'
|
||||||
|
? 'text-green-500'
|
||||||
|
: 'text-red-500'}>
|
||||||
|
{details.status}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">Block</p>
|
||||||
|
<p>{details.blockNumber ?? 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">From</p>
|
||||||
|
<p class="break-all">{details.from}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">To</p>
|
||||||
|
<p class="break-all">{details.to ?? 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">
|
||||||
|
Value ({network?.symbol ?? 'ETH'})
|
||||||
|
</p>
|
||||||
|
<p>{details.value}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">
|
||||||
|
Tx Fee ({network?.symbol ?? 'ETH'})
|
||||||
|
</p>
|
||||||
|
<p>{details.fee}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">
|
||||||
|
Gas Price (Gwei)
|
||||||
|
</p>
|
||||||
|
<p>{details.gasPrice}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">Nonce</p>
|
||||||
|
<p>{details.nonce}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">Type</p>
|
||||||
|
<p>{details.type}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-2">
|
||||||
|
<p class="text-muted-foreground mb-0.5">
|
||||||
|
Input Data (raw)
|
||||||
|
</p>
|
||||||
|
<p class="break-all truncate text-[10px]">
|
||||||
|
{details.input}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="w-full"
|
||||||
|
onclick={() =>
|
||||||
|
electrobun.rpc?.request.openExternal({
|
||||||
|
url: explorerUrl(),
|
||||||
|
})}>
|
||||||
|
View on Explorer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted-foreground text-xs">
|
||||||
|
No details available
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user