remove evm + add xrp
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
|
||||
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import {
|
||||
Card,
|
||||
@@ -13,14 +13,20 @@
|
||||
import { onMount } from 'svelte'
|
||||
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
||||
|
||||
const w = wallet
|
||||
|
||||
let feError = $state<string | null>(null)
|
||||
|
||||
onMount(() => {
|
||||
frontendLog('index.svelte mounted')
|
||||
w.init().catch((e) => frontendLogError('w.init() failed', e))
|
||||
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
||||
xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e))
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (moneroWallet.accountType === 'monero') {
|
||||
navigate('/monero')
|
||||
} else if (xrpWallet.accountType === 'xrp') {
|
||||
navigate('/xrp')
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
@@ -56,61 +62,42 @@
|
||||
{/if}
|
||||
|
||||
<div class="mx-auto mt-24 max-w-sm">
|
||||
{#if !w.ready}
|
||||
<p class="text-center text-muted-foreground text-sm">
|
||||
Loading...
|
||||
</p>
|
||||
{:else if w.accountType}
|
||||
{#if w.accountType === 'multi'}
|
||||
{navigate('/multicoin')}
|
||||
{:else}
|
||||
{navigate('/monero')}
|
||||
{/if}
|
||||
{:else}
|
||||
<Card>
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle>Welcome</CardTitle>
|
||||
<CardDescription>
|
||||
Choose an account type to get started
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Button
|
||||
onclick={async () => {
|
||||
await w.login()
|
||||
navigate('/multicoin')
|
||||
}}
|
||||
class="w-full">
|
||||
<span class="-ml-1 mr-2 flex items-center">
|
||||
<img
|
||||
src="/icons/ethereum.png"
|
||||
alt=""
|
||||
class="relative z-30 size-5 rounded-full ring-2 ring-accent" />
|
||||
<img
|
||||
src="/icons/binance.png"
|
||||
alt=""
|
||||
class="-ml-1.5 relative z-20 size-5 rounded-full ring-2 ring-accent" />
|
||||
<img
|
||||
src="/icons/polygon.png"
|
||||
alt=""
|
||||
class="-ml-1.5 relative z-10 size-5 rounded-full ring-2 ring-accent" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
onclick={async () => {
|
||||
await moneroWallet.login()
|
||||
navigate('/monero')
|
||||
}}
|
||||
variant="outline"
|
||||
class="w-full">
|
||||
<img
|
||||
src="/icons/monero.png"
|
||||
alt=""
|
||||
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
<Card>
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle>Welcome</CardTitle>
|
||||
<CardDescription>
|
||||
Choose an account type to get started
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Button
|
||||
onclick={async () => {
|
||||
await moneroWallet.login()
|
||||
navigate('/monero')
|
||||
}}
|
||||
variant="outline"
|
||||
class="w-full">
|
||||
<img
|
||||
src="/icons/monero.png"
|
||||
alt=""
|
||||
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
|
||||
Monero
|
||||
</Button>
|
||||
<Button
|
||||
onclick={async () => {
|
||||
await xrpWallet.login()
|
||||
navigate('/xrp')
|
||||
}}
|
||||
variant="outline"
|
||||
class="w-full">
|
||||
<img
|
||||
src="/icons/xrp.png"
|
||||
alt=""
|
||||
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
|
||||
XRP
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import EvmDashboard from '$lib/components/EvmDashboard.svelte'
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<EvmDashboard networkId="bsc" />
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import EvmDashboard from '$lib/components/EvmDashboard.svelte'
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<EvmDashboard networkId="eth" />
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import EvmDashboard from '$lib/components/EvmDashboard.svelte'
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<EvmDashboard networkId="polygon" />
|
||||
</div>
|
||||
@@ -1,220 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
|
||||
import { electrobun } from '$lib/electrobun.js'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import { Input } from '$lib/components/ui/input/index.js'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from '$lib/components/ui/select/index.js'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '$lib/components/ui/card/index.js'
|
||||
import { navigate } from 'sv-router/generated'
|
||||
import { parseEther, formatEther, parseGwei } from 'viem'
|
||||
|
||||
const w = wallet
|
||||
|
||||
type TokenOption = {
|
||||
symbol: string
|
||||
balance: string
|
||||
contractAddress: string
|
||||
decimals: number
|
||||
logo?: string
|
||||
}
|
||||
|
||||
const tokens = $derived<TokenOption[]>([
|
||||
{
|
||||
symbol: w.symbol,
|
||||
balance: w.balance,
|
||||
contractAddress: '',
|
||||
decimals: 18,
|
||||
},
|
||||
...w.tokenBalances.map((t) => ({
|
||||
symbol: t.symbol,
|
||||
balance: t.balance,
|
||||
contractAddress: t.contractAddress,
|
||||
decimals: t.decimals,
|
||||
logo: t.logo,
|
||||
})),
|
||||
])
|
||||
|
||||
let selectedSymbol = $state(w.symbol)
|
||||
let recipient = $state('')
|
||||
let amount = $state('')
|
||||
let sending = $state(false)
|
||||
let sentResult = $state<{ txHash: string } | null>(null)
|
||||
let sendError = $state('')
|
||||
let sendAll = $state(false)
|
||||
|
||||
const selectedToken = $derived(
|
||||
tokens.find((t) => t.symbol === selectedSymbol) ?? tokens[0],
|
||||
)
|
||||
|
||||
const exceedsBalance = $derived(
|
||||
amount !== '' &&
|
||||
selectedToken.balance !== '0' &&
|
||||
parseFloat(amount) > parseFloat(selectedToken.balance),
|
||||
)
|
||||
|
||||
const canSend = $derived(
|
||||
recipient.length === 42 &&
|
||||
recipient.startsWith('0x') &&
|
||||
!sending &&
|
||||
(sendAll || (amount !== '' && parseFloat(amount) > 0)),
|
||||
)
|
||||
|
||||
const handleSend = async () => {
|
||||
sending = true
|
||||
sendError = ''
|
||||
sentResult = null
|
||||
try {
|
||||
const isNative = !selectedToken.contractAddress
|
||||
let finalAmount = String(amount)
|
||||
if (sendAll && isNative) {
|
||||
const gasPriceGwei = await electrobun.rpc?.request.fetchGasPrice({ chainid: w.chainid })
|
||||
if (!gasPriceGwei) throw new Error('Could not fetch gas price')
|
||||
const gasWei = parseGwei(gasPriceGwei) * 21000n
|
||||
const balanceWei = parseEther(selectedToken.balance)
|
||||
const sendWei = balanceWei - gasWei
|
||||
if (sendWei <= 0n) throw new Error('Balance too low to cover gas')
|
||||
finalAmount = formatEther(sendWei)
|
||||
} else if (sendAll) {
|
||||
finalAmount = String(selectedToken.balance)
|
||||
}
|
||||
const txHash = await w.send(
|
||||
recipient,
|
||||
finalAmount,
|
||||
selectedToken.contractAddress || undefined,
|
||||
selectedToken.decimals,
|
||||
)
|
||||
sentResult = { txHash }
|
||||
recipient = ''
|
||||
amount = ''
|
||||
sendAll = false
|
||||
} 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(w.network === 'eth' ? '/multicoin' : `/multicoin/${w.network}`)}
|
||||
class="mb-4">
|
||||
← Back
|
||||
</Button>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Send</CardTitle>
|
||||
<CardDescription>
|
||||
Balance: {selectedToken.balance} {selectedToken.symbol}
|
||||
</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">Asset</label>
|
||||
<Select type="single" bind:value={selectedSymbol}>
|
||||
<SelectTrigger>
|
||||
{selectedToken.symbol}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tokens as t}
|
||||
<SelectItem value={t.symbol}>
|
||||
{t.symbol} ({t.balance})
|
||||
</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Recipient Address</label>
|
||||
<Input
|
||||
placeholder="0x..."
|
||||
bind:value={recipient} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Amount ({selectedToken.symbol})</label>
|
||||
<div class="flex gap-2 items-start">
|
||||
<div class="flex-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="0.000000000000000001"
|
||||
min="0"
|
||||
placeholder="0.0"
|
||||
bind:value={amount} />
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => { amount = selectedToken.balance }}
|
||||
class="mt-0 shrink-0">
|
||||
All
|
||||
</Button>
|
||||
</div>
|
||||
{#if exceedsBalance}
|
||||
<p class="text-xs text-red-500">
|
||||
Amount exceeds balance
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !selectedToken.contractAddress}
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={sendAll}
|
||||
class="h-3.5 w-3.5" />
|
||||
<span class="text-xs text-muted-foreground">
|
||||
Send all (deducts estimated gas fee)
|
||||
</span>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<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">
|
||||
<a
|
||||
href="{w.explorerUrl}{sentResult.txHash}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:text-green-500">
|
||||
{sentResult.txHash}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
|
||||
import { electrobun } from '$lib/electrobun.js'
|
||||
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,
|
||||
@@ -12,19 +10,13 @@
|
||||
} from '$lib/components/ui/card/index.js'
|
||||
import { navigate } from 'sv-router/generated'
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left'
|
||||
import Eye from '@lucide/svelte/icons/eye'
|
||||
import Fingerprint from '@lucide/svelte/icons/fingerprint'
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2'
|
||||
import WalletIcon from '@lucide/svelte/icons/wallet'
|
||||
|
||||
const w = wallet
|
||||
const w = xrpWallet
|
||||
let confirm = $state(false)
|
||||
let resetting = $state(false)
|
||||
let deletingWalletId = $state<string | null>(null)
|
||||
let flushing = $state(false)
|
||||
let seedRevealed = $state(false)
|
||||
let seedPassword = $state('')
|
||||
let seedError = $state('')
|
||||
|
||||
async function handleReset() {
|
||||
resetting = true
|
||||
@@ -37,20 +29,22 @@
|
||||
async function handleDeleteWallet(id: string) {
|
||||
await w.deleteWallet(id)
|
||||
deletingWalletId = null
|
||||
if (w.wallets.length === 0) navigate('/multicoin')
|
||||
}
|
||||
|
||||
async function handleFlushCache() {
|
||||
flushing = true
|
||||
await w.flushTxCache()
|
||||
flushing = false
|
||||
if (w.wallets.length === 0) navigate('/xrp')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-16 max-w-md">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onclick={() => navigate(w.network === 'eth' ? '/multicoin' : `/multicoin/${w.network}`)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
w.currentWalletId
|
||||
? navigate('/xrp/wallet/:id', {
|
||||
params: { id: w.currentWalletId },
|
||||
})
|
||||
: navigate('/xrp')}>
|
||||
<ArrowLeft size={16} />
|
||||
Back
|
||||
</Button>
|
||||
@@ -59,7 +53,7 @@
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wallets</CardTitle>
|
||||
<CardDescription>Manage your EVM wallets</CardDescription>
|
||||
<CardDescription>Manage your XRP wallets</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -68,8 +62,8 @@
|
||||
<WalletIcon size={18} 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">
|
||||
Created {new Date(wal.createdAt).toLocaleDateString()}
|
||||
<p class="text-xs text-muted-foreground truncate font-mono">
|
||||
{wal.address}
|
||||
</p>
|
||||
</div>
|
||||
{#if deletingWalletId === wal.id}
|
||||
@@ -92,83 +86,17 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
{#if w.seed}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Seed Phrase</CardTitle>
|
||||
<CardDescription>Reveal your wallet recovery phrase</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if seedRevealed}
|
||||
<div class="rounded-md border border-destructive/50 bg-destructive/5 p-3">
|
||||
<p class="text-xs text-destructive font-medium mb-2">
|
||||
Never share your seed phrase. Anyone with it can access your funds.
|
||||
</p>
|
||||
<p class="font-mono text-xs break-all">{w.seed}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" class="mt-3" onclick={() => { seedRevealed = false; seedPassword = ''; seedError = '' }}>
|
||||
<Eye size={14} /> Hide
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#if w.currentPasswordHash}
|
||||
<div class="flex gap-2 items-end">
|
||||
<Input type="password" placeholder="Enter password" bind:value={seedPassword} />
|
||||
<Button onclick={async () => {
|
||||
seedError = ''
|
||||
const { salt, hash } = JSON.parse(w.currentPasswordHash!)
|
||||
const { hash: check } = await w.hashPassword(seedPassword, salt)
|
||||
if (check === hash) {
|
||||
seedRevealed = true
|
||||
} else {
|
||||
seedError = 'Incorrect password'
|
||||
}
|
||||
seedPassword = ''
|
||||
}}>Reveal</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if w.biometricAvailable}
|
||||
<Button variant="outline" class="w-full" onclick={async () => {
|
||||
seedError = ''
|
||||
const ok = await electrobun.rpc?.request.biometricAuth({ reason: 'Reveal seed phrase' })
|
||||
if (ok) seedRevealed = true
|
||||
}}>
|
||||
<Fingerprint size={14} /> Reveal with Touch ID
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !w.currentPasswordHash && !w.biometricAvailable}
|
||||
<Button onclick={() => (seedRevealed = true)}>Reveal Seed Phrase</Button>
|
||||
{/if}
|
||||
{#if seedError}
|
||||
<p class="text-xs text-destructive">{seedError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Settings</CardTitle>
|
||||
<CardDescription>App preferences and data management</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div>
|
||||
<p class="font-medium text-xs">Transaction Cache</p>
|
||||
<p class="text-muted-foreground text-xs mt-1">
|
||||
Clears all cached transaction history and sync status. Data will be re-fetched from Alchemy on next refresh.
|
||||
</p>
|
||||
<div class="mt-3">
|
||||
<Button variant="outline" onclick={handleFlushCache} disabled={flushing}>
|
||||
{flushing ? 'Flushing...' : 'Flush Transaction Cache'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-xs">Reset App</p>
|
||||
<p class="text-muted-foreground text-xs mt-1">
|
||||
Removes your seed, API key, and all cached data from the keychain
|
||||
and database. You can then set up the wallet fresh.
|
||||
Removes all XRP wallets and seeds from the keychain. You
|
||||
can then set up the wallet fresh.
|
||||
</p>
|
||||
<div class="mt-3">
|
||||
{#if confirm}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
electrobun,
|
||||
type TransactionDetails,
|
||||
} from '$lib/electrobun.js'
|
||||
import { evmWallet } from '$lib/states/evm-wallet.svelte.js'
|
||||
import { networks } from '$lib/states/evm-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,
|
||||
address: evmWallet.address || undefined,
|
||||
})
|
||||
.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('/multicoin')}
|
||||
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>
|
||||
{#if details.pairedValue}
|
||||
<div
|
||||
class="rounded-md border border-yellow-500/30 bg-yellow-500/5 p-2">
|
||||
<p class="mb-0.5 flex items-center gap-1 text-xs font-medium text-yellow-600">
|
||||
Swap
|
||||
</p>
|
||||
<p class="font-mono text-xs">
|
||||
{Number(details.value).toFixed(4)}
|
||||
{network?.symbol ?? 'ETH'}
|
||||
<span class="mx-1">→</span>
|
||||
{Number(details.pairedValue).toFixed(4)}
|
||||
{details.pairedSymbol ?? network?.symbol ?? 'ETH'}
|
||||
</p>
|
||||
</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">
|
||||
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>
|
||||
<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>
|
||||
@@ -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