remove evm + add xrp
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
<script lang="ts">
|
||||
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import { Input } from '$lib/components/ui/input/index.js'
|
||||
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '$lib/components/ui/card/index.js'
|
||||
import Fingerprint from '@lucide/svelte/icons/fingerprint'
|
||||
import LockKeyhole from '@lucide/svelte/icons/lock-keyhole'
|
||||
import Plus from '@lucide/svelte/icons/plus'
|
||||
import WalletIcon from '@lucide/svelte/icons/wallet'
|
||||
import Loader from '$lib/components/loader.svelte'
|
||||
import { navigate } from 'sv-router/generated'
|
||||
|
||||
const w = xrpWallet
|
||||
|
||||
let inputWalletName = $state('')
|
||||
let inputSeed = $state('')
|
||||
let inputPassword = $state('')
|
||||
let inputUnlockPassword = $state('')
|
||||
let showCreateForm = $state(false)
|
||||
let newSeed = $state<string | null>(null)
|
||||
|
||||
let initStarted = false
|
||||
|
||||
$effect(() => {
|
||||
if (!initStarted) {
|
||||
initStarted = true
|
||||
w.init().then(async () => {
|
||||
if (w.accountType === 'xrp') return
|
||||
await w.login()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (w.accountType === 'xrp' && w.currentWalletId && w.seed && !newSeed) {
|
||||
navigate('/xrp/wallet/:id', {
|
||||
params: { id: w.currentWalletId },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const handleCreate = async () => {
|
||||
const seed = await w.createWallet(
|
||||
inputWalletName,
|
||||
inputPassword || undefined,
|
||||
)
|
||||
newSeed = seed ?? null
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
await w.importWallet(
|
||||
inputWalletName,
|
||||
inputSeed.trim(),
|
||||
inputPassword || undefined,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if !w.ready}
|
||||
<p class="text-center text-muted-foreground text-sm mt-8">
|
||||
Loading...
|
||||
</p>
|
||||
{:else if !w.accountType}
|
||||
<p class="text-center text-muted-foreground text-sm mt-8">
|
||||
<a href="/" class="underline">Go to welcome page</a>
|
||||
to get started
|
||||
</p>
|
||||
{:else}
|
||||
<a
|
||||
href="/"
|
||||
class="text-xs text-muted-foreground underline mb-1 inline-block">
|
||||
← Back to home
|
||||
</a>
|
||||
{#if newSeed}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wallet Created</CardTitle>
|
||||
<CardDescription>
|
||||
Save this seed phrase — it is the only way to recover
|
||||
your wallet
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="rounded-md border border-destructive/50 bg-destructive/5 p-3 mb-3">
|
||||
<p class="text-xs text-destructive font-medium mb-2">
|
||||
Never share your seed. Anyone with it can access your
|
||||
funds.
|
||||
</p>
|
||||
<p class="font-mono text-xs break-all">{newSeed}</p>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mb-3">
|
||||
Your XRP address becomes active after it receives at
|
||||
least 10 XRP (the base reserve).
|
||||
</p>
|
||||
<Button
|
||||
class="w-full"
|
||||
onclick={() => {
|
||||
newSeed = null
|
||||
inputWalletName = ''
|
||||
inputPassword = ''
|
||||
}}>
|
||||
I saved my seed
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if w.wallets.length === 0 || showCreateForm}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{showCreateForm ? 'Add Another Wallet' : 'Create / Import Wallet'}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Generate a new XRP wallet or import an existing seed
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Input
|
||||
placeholder="Wallet name (e.g. Main, Savings)"
|
||||
bind:value={inputWalletName} />
|
||||
<Button
|
||||
onclick={handleCreate}
|
||||
disabled={w.loading || !inputWalletName.trim()}>
|
||||
{w.loading ? 'Creating...' : 'Generate New Wallet'}
|
||||
</Button>
|
||||
<hr class="border-muted" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Or import an existing wallet:
|
||||
</p>
|
||||
<Textarea
|
||||
placeholder="XRP seed (s...) or 12/24-word phrase"
|
||||
bind:value={inputSeed} />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Set a password (optional)"
|
||||
bind:value={inputPassword} />
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={handleImport}
|
||||
disabled={w.loading ||
|
||||
!inputSeed.trim() ||
|
||||
!inputWalletName.trim()}>
|
||||
{w.loading ? 'Importing...' : 'Import Wallet'}
|
||||
</Button>
|
||||
{#if showCreateForm}
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => (showCreateForm = false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if w.error}
|
||||
<p class="mt-3 text-xs text-red-500">{w.error}</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if !w.currentWalletId}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wallets</CardTitle>
|
||||
<CardDescription>Select a wallet to use</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each w.wallets as wal}
|
||||
<button
|
||||
onclick={async () => {
|
||||
if (wal.hasPassword) {
|
||||
await w.selectWallet(wal.id)
|
||||
} else {
|
||||
await w.selectAndUnlockWallet(wal.id)
|
||||
}
|
||||
}}
|
||||
class="flex w-full cursor-pointer items-center gap-3 rounded-md border border-input p-3 text-left hover:bg-muted transition-colors">
|
||||
<WalletIcon
|
||||
size={20}
|
||||
class="shrink-0 text-muted-foreground" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm">{wal.name}</p>
|
||||
<p class="text-xs text-muted-foreground truncate font-mono">
|
||||
{wal.address}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if wal.hasPassword}
|
||||
<LockKeyhole
|
||||
size={14}
|
||||
class="text-muted-foreground" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => (showCreateForm = true)}
|
||||
class="mt-2">
|
||||
<Plus size={16} />
|
||||
Add Wallet
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if w.isLocked}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{w.currentWallet?.name ?? 'Wallet'} Locked
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Unlock to access your wallet
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#if w.biometricAvailable}
|
||||
<Button
|
||||
onclick={() => w.unlockWithBiometrics()}
|
||||
disabled={w.loading}>
|
||||
<Fingerprint size={16} />
|
||||
{w.loading ? 'Unlocking...' : 'Unlock with Touch ID'}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if w.currentPasswordHash}
|
||||
<div class="flex gap-2 items-end">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
bind:value={inputUnlockPassword} />
|
||||
<Button
|
||||
onclick={async () => {
|
||||
const ok = await w.unlockWithPassword(
|
||||
inputUnlockPassword,
|
||||
)
|
||||
if (!ok) w.error = 'Wrong password'
|
||||
inputUnlockPassword = ''
|
||||
}}
|
||||
disabled={w.loading || !inputUnlockPassword}
|
||||
size="sm">
|
||||
{#if w.loading}
|
||||
<Loader />
|
||||
{:else}
|
||||
Unlock
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => w.clearSelection()}>
|
||||
Pick another wallet
|
||||
</Button>
|
||||
</div>
|
||||
{#if w.error}
|
||||
<p class="mt-3 text-xs text-red-500">{w.error}</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script lang="ts">
|
||||
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import { Input } from '$lib/components/ui/input/index.js'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '$lib/components/ui/card/index.js'
|
||||
import { navigate } from 'sv-router/generated'
|
||||
|
||||
const w = xrpWallet
|
||||
|
||||
let recipient = $state('')
|
||||
let destinationTag = $state('')
|
||||
let amountXrp = $state('')
|
||||
let sending = $state(false)
|
||||
let sentResult = $state<{ hash: string; fee: string } | null>(null)
|
||||
let sendError = $state('')
|
||||
|
||||
const isValidAddress = $derived(/^r[1-9A-HJ-NP-Za-km-z]{24,34}$/.test(recipient))
|
||||
|
||||
const amount = $derived(parseFloat(amountXrp))
|
||||
const amountValid = $derived(!isNaN(amount) && amount > 0)
|
||||
const exceedsBalance = $derived(amountValid && amount > parseFloat(w.balance))
|
||||
const tagValid = $derived(
|
||||
destinationTag === '' ||
|
||||
(/^\d+$/.test(destinationTag) && Number(destinationTag) <= 4294967295),
|
||||
)
|
||||
const canSend = $derived(
|
||||
isValidAddress &&
|
||||
amountValid &&
|
||||
!exceedsBalance &&
|
||||
tagValid &&
|
||||
!sending,
|
||||
)
|
||||
|
||||
const handleSend = async () => {
|
||||
sending = true
|
||||
sendError = ''
|
||||
sentResult = null
|
||||
try {
|
||||
const result = await w.send(
|
||||
recipient,
|
||||
amount.toString(),
|
||||
destinationTag === '' ? undefined : Number(destinationTag),
|
||||
)
|
||||
sentResult = result
|
||||
recipient = ''
|
||||
destinationTag = ''
|
||||
amountXrp = ''
|
||||
} catch (e) {
|
||||
sendError = e instanceof Error ? e.message : 'Send failed'
|
||||
} finally {
|
||||
sending = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
navigate('/xrp/wallet/:id', {
|
||||
params: { id: w.currentWalletId ?? '' },
|
||||
})}
|
||||
class="mb-4">
|
||||
← Back
|
||||
</Button>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Send XRP</CardTitle>
|
||||
<CardDescription>
|
||||
Balance: {w.balance} XRP
|
||||
{#if w.fee}
|
||||
(fee: ~{w.fee} XRP)
|
||||
{/if}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}}
|
||||
class="space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Recipient Address</label>
|
||||
<Input
|
||||
placeholder="XRP address starting with r..."
|
||||
bind:value={recipient} />
|
||||
{#if recipient && !isValidAddress}
|
||||
<p class="text-xs text-red-500">Invalid XRP address</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">
|
||||
Destination Tag (optional)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="Required by some exchanges"
|
||||
bind:value={destinationTag} />
|
||||
{#if !tagValid}
|
||||
<p class="text-xs text-red-500">
|
||||
Destination tag must be a number between 0 and
|
||||
4294967295
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Amount (XRP)</label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
placeholder="0.0"
|
||||
bind:value={amountXrp} />
|
||||
<span
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">
|
||||
XRP
|
||||
</span>
|
||||
</div>
|
||||
{#if exceedsBalance}
|
||||
<p class="text-xs text-red-500">
|
||||
Amount exceeds balance
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={!canSend} class="w-full">
|
||||
{sending ? 'Sending...' : 'Send'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{#if sendError}
|
||||
<p class="mt-3 text-xs text-red-500">{sendError}</p>
|
||||
{/if}
|
||||
|
||||
{#if sentResult}
|
||||
<div
|
||||
class="mt-4 space-y-2 rounded-md border border-green-500/30 bg-green-500/5 p-3">
|
||||
<p class="text-xs font-medium text-green-600">
|
||||
Transaction Sent
|
||||
</p>
|
||||
<p class="font-mono text-xs break-all">
|
||||
Hash: {sentResult.hash}
|
||||
</p>
|
||||
<p class="font-mono text-xs">
|
||||
Fee: {sentResult.fee} XRP
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import { electrobun, type XrpTxDetails } from '$lib/electrobun.js'
|
||||
import { xrpWallet, xrpNetwork } from '$lib/states/xrp-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<XrpTxDetails | 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
|
||||
.xrpGetTxDetails({ 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('/xrp/wallet/:id', {
|
||||
params: { id: xrpWallet.currentWalletId ?? '' },
|
||||
})}
|
||||
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.to === xrpWallet.address ? 'text-green-500' : 'text-muted-foreground'}">
|
||||
{#if details.to === xrpWallet.address}
|
||||
<ArrowDown size={16} />
|
||||
{:else}
|
||||
<ArrowUp size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
{details.to === xrpWallet.address ? '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="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}</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>{details.amount} XRP</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Fee</p>
|
||||
<p>{details.fee} XRP</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">Ledger</p>
|
||||
<p>{details.ledgerIndex.toLocaleString()}</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Validated</p>
|
||||
<p>{details.confirmed ? 'Yes' : 'No'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if details.destinationTag !== undefined}
|
||||
<div class="rounded-md bg-muted p-2">
|
||||
<p class="text-muted-foreground mb-0.5">Destination Tag</p>
|
||||
<p>{details.destinationTag}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#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}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
onclick={() =>
|
||||
electrobun.rpc?.request.openExternal({
|
||||
url: `${xrpNetwork.explorerUrl}${details?.hash}`,
|
||||
})}>
|
||||
View on Explorer
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-xs">No details available</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -0,0 +1,235 @@
|
||||
<script lang="ts">
|
||||
import { xrpWallet, xrpNetwork } from '$lib/states/xrp-wallet.svelte.js'
|
||||
import { electrobun } from '$lib/electrobun.js'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
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'
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings'
|
||||
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
||||
import ExternalLink from '@lucide/svelte/icons/external-link'
|
||||
import Loader from '$lib/components/loader.svelte'
|
||||
import QrCode from '$lib/components/qr.svelte'
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js'
|
||||
import { CopyButton } from '$lib/components/ui/copy-button'
|
||||
import { navigate, route } from 'sv-router/generated'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
const w = xrpWallet
|
||||
|
||||
const walletId = $derived(route.params.id)
|
||||
|
||||
let qrDialogOpen = $state(false)
|
||||
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined =
|
||||
$state()
|
||||
|
||||
$effect(() => {
|
||||
if (w.seed && !autoRefreshTimer) {
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
w.refresh()
|
||||
}, 30000)
|
||||
} else if (!w.seed && autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer)
|
||||
autoRefreshTimer = undefined
|
||||
}
|
||||
return () => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer)
|
||||
autoRefreshTimer = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (!w.seed || w.currentWalletId !== walletId) {
|
||||
navigate('/xrp')
|
||||
return
|
||||
}
|
||||
w.refresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle>
|
||||
{w.currentWallet?.name ?? 'XRP Wallet'}
|
||||
</CardTitle>
|
||||
<img src="/icons/xrp.png" alt="" class="size-6 rounded-full" />
|
||||
</div>
|
||||
<CardDescription>XRP Ledger</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="mb-4 space-y-1">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="min-w-0 flex-1 space-y-1">
|
||||
<p class="font-medium text-xs">Address</p>
|
||||
<p class="font-mono text-xs break-all">{w.address}</p>
|
||||
<div class="flex gap-1.5 mt-1">
|
||||
<Dialog.Root bind:open={qrDialogOpen}>
|
||||
<Dialog.Trigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
class="text-muted-foreground"
|
||||
aria-label="Show QR code">
|
||||
<QrCodeIcon size={16} />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div
|
||||
class="flex flex-col items-center gap-3 py-4">
|
||||
<QrCode text={w.address} size={256} />
|
||||
<p
|
||||
class="font-mono text-xs break-all text-center max-w-64">
|
||||
{w.address}
|
||||
</p>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<CopyButton
|
||||
text={w.address}
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
class="text-muted-foreground">
|
||||
</CopyButton>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
class="text-muted-foreground"
|
||||
aria-label="View on explorer"
|
||||
onclick={() =>
|
||||
electrobun.rpc?.request.openExternal({
|
||||
url: w.explorerAddressUrl,
|
||||
})}>
|
||||
<ExternalLink size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 font-medium text-xs">Balance</p>
|
||||
{#if w.loadingBalance}
|
||||
<div class="py-1"><Loader /></div>
|
||||
{:else}
|
||||
<div class="flex items-baseline gap-2">
|
||||
<p class="font-mono text-lg">{w.balance} XRP</p>
|
||||
{#if w.price}
|
||||
<p class="font-mono text-sm text-muted-foreground">
|
||||
≈ ${(
|
||||
parseFloat(w.balance) * parseFloat(w.price)
|
||||
).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !w.funded}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Account not activated — it needs at least 10 XRP to
|
||||
meet the base reserve.
|
||||
</p>
|
||||
{/if}
|
||||
{#if w.fee}
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
Network fee: ~{w.fee} XRP
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => navigate('/xrp/send')}>
|
||||
Send
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => navigate('/settings')}>
|
||||
<SettingsIcon size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={async () => {
|
||||
await w.logout()
|
||||
navigate('/')
|
||||
}}>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transactions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if w.loadingTransactions}
|
||||
<div class="flex justify-center py-4">
|
||||
<Loader />
|
||||
</div>
|
||||
{:else if w.transactions.length === 0}
|
||||
<p class="text-muted-foreground text-xs">
|
||||
No transactions found
|
||||
</p>
|
||||
{:else}
|
||||
<div class="max-h-96 space-y-1 overflow-y-auto">
|
||||
{#each w.transactions as tx}
|
||||
<button
|
||||
onclick={() =>
|
||||
navigate('/xrp/tx/:hash', {
|
||||
params: { hash: 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} />
|
||||
{:else}
|
||||
<ArrowUp size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 space-y-0.5">
|
||||
<p class="font-medium">
|
||||
{tx.direction === 'in' ? 'Received' : 'Sent'}
|
||||
{#if !tx.confirmed}
|
||||
<span class="text-muted-foreground">
|
||||
(unconfirmed)
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
<p class="font-mono text-muted-foreground">
|
||||
{tx.amount} XRP
|
||||
</p>
|
||||
{#if tx.timestamp && tx.timestamp !== '0'}
|
||||
<p class="text-muted-foreground">
|
||||
{new Date(
|
||||
Number(tx.timestamp) * 1000,
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
{/if}
|
||||
<p
|
||||
class="text-muted-foreground/50 truncate font-mono">
|
||||
{tx.hash.slice(0, 12)}...
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user