Add Monero transaction details page and chain icons on EVM dashboard
This commit is contained in:
@@ -727,6 +727,14 @@ const rpc = BrowserView.defineRPC<RPC>({
|
||||
console.log('[rpc] moneroTransfer complete:', result.txHash)
|
||||
return result
|
||||
},
|
||||
moneroGetTransferDetails: async ({ txid }) => {
|
||||
console.log('[rpc] moneroGetTransferDetails:', txid)
|
||||
if (!moneroManager)
|
||||
throw new Error('Monero wallet RPC not started')
|
||||
const details = await moneroManager.getTransferDetails(txid)
|
||||
console.log('[rpc] moneroGetTransferDetails complete:', details?.hash)
|
||||
return details
|
||||
},
|
||||
evmCreateWallet: async ({ name, phrase, passwordHash }) => {
|
||||
console.log('[rpc] evmCreateWallet:', name)
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
@@ -305,6 +305,38 @@ export class MoneroWalletManager {
|
||||
return all
|
||||
}
|
||||
|
||||
async getTransferDetails(txid: string): Promise<any> {
|
||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||
const result = await this.rawRpc('get_transfer_by_txid', { txid })
|
||||
if (!result?.transfer) return null
|
||||
const t = result.transfer
|
||||
const isIn = t.type === 'in'
|
||||
const isOut = t.type === 'out'
|
||||
const dests = t.destinations?.map((d: any) => ({
|
||||
address: d.address,
|
||||
amount: d.amount?.toString() ?? '0',
|
||||
})) ?? []
|
||||
const subaddrIndices = isIn
|
||||
? [{ major: t.subaddr_index?.major ?? 0, minor: t.subaddr_index?.minor ?? 0 }]
|
||||
: (t.subaddr_indices ?? []).map((s: any) => ({ major: s.major ?? 0, minor: s.minor ?? 0 }))
|
||||
return {
|
||||
hash: t.txid,
|
||||
direction: isIn ? ('in' as const) : ('out' as const),
|
||||
amount: t.amount?.toString() ?? '0',
|
||||
fee: t.fee?.toString() ?? '0',
|
||||
height: t.height ?? 0,
|
||||
timestamp: String(t.timestamp ?? 0),
|
||||
confirmations: t.confirmations ?? 0,
|
||||
unlockTime: t.unlock_time ?? 0,
|
||||
locked: t.locked ?? false,
|
||||
doubleSpend: t.double_spend_seen ?? false,
|
||||
note: t.note,
|
||||
paymentId: t.payment_id ?? '',
|
||||
destinations: dests,
|
||||
subaddrIndices,
|
||||
}
|
||||
}
|
||||
|
||||
async getHeight(): Promise<number> {
|
||||
if (!this.wallet) throw new Error('Wallet RPC not started')
|
||||
const height = (await this.wallet.getHeight()) as number
|
||||
|
||||
@@ -261,6 +261,12 @@
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
|
||||
onclick={() => navigate(navTarget(net.id))}>
|
||||
<img
|
||||
src={nativeIcon(net.id)}
|
||||
alt=""
|
||||
class="-ml-0.5 mr-1 inline-block size-3.5 rounded-full align-middle"
|
||||
onerror={(e) =>
|
||||
((e.target as HTMLElement).style.display = 'none')} />
|
||||
{net.name}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -8,6 +8,7 @@ export type {
|
||||
MoneroSubaddressEntry,
|
||||
MoneroAccountEntry,
|
||||
MoneroSendResult,
|
||||
MoneroTransferDetails,
|
||||
TxEntry,
|
||||
TransactionDetails,
|
||||
TokenBalanceResult,
|
||||
|
||||
@@ -46,6 +46,23 @@ export type MoneroSendResult = {
|
||||
amount: string
|
||||
}
|
||||
|
||||
export type MoneroTransferDetails = {
|
||||
hash: string
|
||||
direction: 'in' | 'out'
|
||||
amount: string
|
||||
fee: string
|
||||
height: number
|
||||
timestamp: string
|
||||
confirmations: number
|
||||
unlockTime: number
|
||||
locked: boolean
|
||||
doubleSpend: boolean
|
||||
note?: string
|
||||
paymentId: string
|
||||
destinations: { address: string; amount: string }[]
|
||||
subaddrIndices: { major: number; minor: number }[]
|
||||
}
|
||||
|
||||
export type MoneroAccountEntry = {
|
||||
index: number
|
||||
primaryAddress: string
|
||||
@@ -137,6 +154,7 @@ export type RPC = {
|
||||
moneroGetAccounts: { params: {}; response: MoneroAccountEntry[] }
|
||||
moneroListWallets: { params: {}; response: string[] }
|
||||
moneroTransfer: { params: { address: string; amount: string; priority?: number; accountIndex?: number }; response: MoneroSendResult }
|
||||
moneroGetTransferDetails: { params: { txid: string }; response: MoneroTransferDetails | null }
|
||||
evmCreateWallet: { params: { name: string; phrase: string; passwordHash?: string }; response: { id: string; name: string; createdAt: string } }
|
||||
evmListWallets: { params: {}; response: EvmWalletInfo[] }
|
||||
evmGetSeed: { params: { vaultKey: string }; response: string }
|
||||
|
||||
@@ -266,7 +266,9 @@
|
||||
{:else}
|
||||
<div class="max-h-96 space-y-1 overflow-y-auto">
|
||||
{#each w.txs as tx}
|
||||
<div class="flex items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs">
|
||||
<button
|
||||
onclick={() => navigate(`/monero/tx/${tx.hash}`)}
|
||||
class="flex w-full items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs text-left cursor-pointer hover:bg-muted/80 transition-colors">
|
||||
<div class="shrink-0 mt-0.5 {tx.direction === 'in' ? 'text-green-500' : 'text-muted-foreground'}">
|
||||
{#if tx.direction === 'in'}
|
||||
<ArrowDown size={16} />
|
||||
@@ -288,7 +290,7 @@
|
||||
{tx.hash.slice(0, 12)}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<script lang="ts">
|
||||
import { electrobun, type MoneroTransferDetails } from '$lib/electrobun.js'
|
||||
import { atomicToXmr } from '$lib/states/monero-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'
|
||||
import ArrowDown from '@lucide/svelte/icons/arrow-down'
|
||||
import ArrowUp from '@lucide/svelte/icons/arrow-up'
|
||||
|
||||
let details = $state<MoneroTransferDetails | null>(null)
|
||||
let loading = $state(true)
|
||||
let error = $state('')
|
||||
|
||||
const hash = $derived(route.params.hash)
|
||||
|
||||
$effect(() => {
|
||||
if (!hash) return
|
||||
loading = true
|
||||
error = ''
|
||||
electrobun.rpc?.request
|
||||
.moneroGetTransferDetails({ txid: hash })
|
||||
.then((res) => {
|
||||
details = res
|
||||
loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch details'
|
||||
loading = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => navigate('/monero')}
|
||||
class="mb-4">
|
||||
← Back
|
||||
</Button>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transaction Details</CardTitle>
|
||||
</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="flex items-center gap-2 rounded-md bg-muted p-2">
|
||||
<div class="shrink-0 {details.direction === 'in' ? 'text-green-500' : 'text-muted-foreground'}">
|
||||
{#if details.direction === 'in'}
|
||||
<ArrowDown size={16} />
|
||||
{:else}
|
||||
<ArrowUp size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
{details.direction === 'in' ? 'Received' : 'Sent'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Transaction 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">Amount</p>
|
||||
<p>{atomicToXmr(details.amount)} XMR</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Fee</p>
|
||||
<p>{atomicToXmr(details.fee)} XMR</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">Height</p>
|
||||
<p>{details.height.toLocaleString()}</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Confirmations</p>
|
||||
<p>{details.confirmations.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if details.timestamp && details.timestamp !== '0'}
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Date</p>
|
||||
<p>{new Date(Number(details.timestamp) * 1000).toLocaleString()}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if details.direction === 'out' && details.destinations.length > 0}
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Destinations</p>
|
||||
{#each details.destinations as dest}
|
||||
<p class="break-all text-muted-foreground/70">{dest.address}</p>
|
||||
<p class="text-muted-foreground">{atomicToXmr(dest.amount)} XMR</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Locked</p>
|
||||
<p>{details.locked ? 'Yes' : 'No'}</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Double Spend</p>
|
||||
<p>{details.doubleSpend ? 'Yes' : 'No'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if details.note}
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Note</p>
|
||||
<p>{details.note}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if details.paymentId}
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Payment ID</p>
|
||||
<p class="break-all">{details.paymentId}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
onclick={() =>
|
||||
electrobun.rpc?.request.openExternal({
|
||||
url: `https://moneroblocks.info/tx/${details.hash}`,
|
||||
})}>
|
||||
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