create accounts/subaddresses, chunked address style, fix canSend for subaddresses

This commit is contained in:
2026-05-26 13:33:28 +08:00
parent 34602a4bfa
commit 2139d5e6ff
7 changed files with 259 additions and 12 deletions
+183 -11
View File
@@ -7,6 +7,13 @@
import { Button } from '$lib/components/ui/button/index.js'
import { Textarea } from '$lib/components/ui/textarea/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '$lib/components/ui/select/index.js'
import * as Dialog from '$lib/components/ui/dialog/index.js'
import {
Card,
CardContent,
@@ -31,6 +38,13 @@
} catch {}
}
const chunkAddress = (addr: string) => {
const start = addr.slice(0, 6)
const mid = addr.slice(6, 12)
const end = addr.slice(-6)
return { start, mid, end }
}
let moneroWalletName = $state('')
let moneroWalletPass = $state('')
let moneroMnemonic = $state('')
@@ -48,6 +62,12 @@
estimatedFee: string
} | null>(null)
let accountDialogOpen = $state(false)
let accountLabel = $state('')
let subaddressDialogOpen = $state(false)
let subaddressAccountIndex = $state('')
let subaddressLabel = $state('')
$effect(() => {
if (w.walletOpen && !autoRefreshTimer) {
autoRefreshTimer = setInterval(() => w.refresh(), 30000)
@@ -91,7 +111,7 @@
{: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
to get started
</p>
{:else if w.accountType === 'monero'}
{#if w.downloading}
@@ -283,6 +303,7 @@
</Card>
{:else}
<!-- Monero wallet unlocked -->
{@const a = chunkAddress(w.address)}
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -313,7 +334,14 @@
<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}
<span class="text-primary-foreground">
{a.start}
</span>
<span class="text-muted-foreground">{a.mid}</span>
<span class="text-muted-foreground/40">...</span>
<span class="text-primary-foreground">
{a.end}
</span>
</p>
</div>
</div>
@@ -325,12 +353,19 @@
</p>
{/if}
{#if feeEstimate}
{@const labels = ['Unimportant', 'Normal', 'Elevated', 'Priority']}
{@const labels = [
'Unimportant',
'Normal',
'Elevated',
'Priority',
]}
<div class="mt-1 space-y-0.5">
{#each feeEstimate.fees as f, i}
{@const feeAtomic = (BigInt(f) * 2500n) / 1000n}
<p class="text-xs text-muted-foreground">
{labels[i]}: ~{atomicToXmr(feeAtomic.toString())} XMR
{labels[i]}: ~{atomicToXmr(
feeAtomic.toString(),
)} XMR
</p>
{/each}
</div>
@@ -408,7 +443,12 @@
<div class="max-h-96 space-y-1 overflow-y-auto">
{#each w.txs as tx}
<button
onclick={() => navigate(`/monero/tx/${tx.hash}`)}
onclick={() =>
navigate(`/monero/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'
@@ -457,7 +497,121 @@
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex gap-2 mb-3">
<Dialog.Root bind:open={accountDialogOpen}>
<Dialog.Trigger>
<Button size="sm" variant="outline">
+ Account
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Create Account</Dialog.Title>
<Dialog.Description>
Optional label for the new account
</Dialog.Description>
</Dialog.Header>
<div class="px-6 pb-4 space-y-3">
<Input
placeholder="Account label (optional)"
bind:value={accountLabel} />
<div class="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onclick={() => {
accountDialogOpen = false
accountLabel = ''
}}>
Cancel
</Button>
<Button
size="sm"
onclick={async () => {
await w.createAccount(
accountLabel || undefined,
)
accountDialogOpen = false
accountLabel = ''
}}>
Create
</Button>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root bind:open={subaddressDialogOpen}>
<Dialog.Trigger>
<Button size="sm" variant="outline">
+ Subaddress
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Create Subaddress</Dialog.Title>
<Dialog.Description>
Account index and optional label
</Dialog.Description>
</Dialog.Header>
<div class="px-6 pb-4 space-y-3">
<div class="space-y-1.5">
<label class="text-xs font-medium">
Account
</label>
<Select
type="single"
bind:value={subaddressAccountIndex}>
<SelectTrigger>
{subaddressAccountIndex
? `Account ${subaddressAccountIndex}`
: 'Select account'}
</SelectTrigger>
<SelectContent>
{#each w.accounts as acct}
<SelectItem value={String(acct.index)}>
Account {acct.index}{acct.label
? ` - ${acct.label}`
: ''}
</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<Input
placeholder="Subaddress label (optional)"
bind:value={subaddressLabel} />
<div class="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onclick={() => {
subaddressDialogOpen = false
subaddressAccountIndex = ''
subaddressLabel = ''
}}>
Cancel
</Button>
<Button
size="sm"
disabled={!subaddressAccountIndex}
onclick={async () => {
await w.createSubaddress(
parseInt(subaddressAccountIndex),
subaddressLabel || undefined,
)
subaddressDialogOpen = false
subaddressAccountIndex = ''
subaddressLabel = ''
}}>
Create
</Button>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
</div>
{#each w.accounts as account}
{@const a = chunkAddress(account.primaryAddress)}
<div class="mb-3 rounded-md bg-muted p-3">
<div class="mb-1 flex items-center justify-between">
<p class="text-xs font-medium">
@@ -469,10 +623,16 @@
</div>
<p
class="mb-2 font-mono text-xs text-muted-foreground break-all">
{account.primaryAddress}
<span class="text-primary-foreground">
{a.start}
</span>
<span class="text-muted-foreground">{a.mid}</span>
<span class="text-muted-foreground/40">...</span>
<span class="text-primary-foreground">{a.end}</span>
</p>
<button
onclick={() => copyToClipboard(account.primaryAddress)}
onclick={() =>
copyToClipboard(account.primaryAddress)}
class="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors">
<Copy size={12} /> Copy address
</button>
@@ -482,6 +642,7 @@
Subaddresses
</p>
{#each account.subaddresses as sub}
{@const a = chunkAddress(sub.address)}
<div
class="rounded-sm bg-background px-2 py-1.5 text-xs">
<div class="flex items-start gap-2">
@@ -498,12 +659,23 @@
{atomicToXmr(sub.balance)} XMR
</p>
</div>
<p
class="font-mono text-muted-foreground/50 truncate mt-0.5">
{sub.address}
<p class="font-mono text-xs mt-0.5">
<span class="text-primary-foreground">
{a.start}
</span>
<span class="text-muted-foreground">
{a.mid}
</span>
<span class="text-muted-foreground/40">
...
</span>
<span class="text-primary-foreground">
{a.end}
</span>
</p>
<button
onclick={() => copyToClipboard(sub.address)}
onclick={() =>
copyToClipboard(sub.address)}
class="mt-1 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors">
<Copy size={12} /> Copy
</button>