remove multiple wallets + monero auto-open + agents rules

This commit is contained in:
2026-08-01 19:12:32 +08:00
parent bd8ba8e3e5
commit 5ac8c93027
7 changed files with 114 additions and 352 deletions
+2
View File
@@ -2,3 +2,5 @@
- Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command. - Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command.
- Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location. - Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location.
- Only commit when explicitly told to do so. When asked to commit, always write a single simple line commit message.
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending."
@@ -82,6 +82,10 @@ export const MoneroWallet = () => {
await checkStatus() await checkStatus()
} }
await listWallets() await listWallets()
if (wallets.length > 0) {
const pw = await moneroGetStoredPassword(wallets[0])
if (pw) await openExistingWallet(wallets[0], pw)
}
} }
const logout = async () => { const logout = async () => {
@@ -80,7 +80,7 @@ export const XrpWallet = () => {
) )
if (walletList) { if (walletList) {
wallets = walletList wallets = walletList
if (walletList.length === 1 && !currentWalletId) { if (walletList.length > 0 && !currentWalletId) {
currentWalletId = walletList[0].id currentWalletId = walletList[0].id
} }
} }
+1 -34
View File
@@ -269,40 +269,7 @@
</button> </button>
</div> </div>
{#if xrpWallet.wallets.length > 1} <div class="flex gap-3">
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-sm">XRP Wallets</CardTitle>
</CardHeader>
<CardContent>
<div class="space-y-1">
{#each xrpWallet.wallets as wal}
<button
onclick={() => {
xrpWallet.selectAndUnlockWallet(wal.id)
navigate('/xrp')
}}
class="flex w-full items-center gap-3 rounded-lg p-2 text-left hover:bg-muted transition-colors">
<WalletIcon size={16} class="shrink-0 text-muted-foreground" />
<div class="flex-1 min-w-0">
<p class="text-sm font-medium">{wal.name}</p>
<p class="text-xs text-muted-foreground truncate font-mono">{wal.address}</p>
</div>
{#if xrpBalances[wal.address]?.funded === false}
<span class="text-xs text-muted-foreground">unfunded</span>
{:else if xrpBalances[wal.address]}
<span class="font-mono text-sm tabular-nums">
{fmtBal(xrpBalances[wal.address].balance)}
</span>
{/if}
</button>
{/each}
</div>
</CardContent>
</Card>
{/if}
<div class="flex gap-3">
<Button <Button
onclick={() => navigate('/xrp')} onclick={() => navigate('/xrp')}
variant="outline" variant="outline"
+64 -157
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js' import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
import { session } from '$lib/states/session.svelte.js'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { Textarea } from '$lib/components/ui/textarea/index.js' import { Textarea } from '$lib/components/ui/textarea/index.js'
import { Input } from '$lib/components/ui/input/index.js' import { Input } from '$lib/components/ui/input/index.js'
@@ -11,28 +10,21 @@
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '$lib/components/ui/card/index.js' } from '$lib/components/ui/card/index.js'
import Fingerprint from '@lucide/svelte/icons/fingerprint'
import Loader from '$lib/components/loader.svelte' import Loader from '$lib/components/loader.svelte'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
const w = moneroWallet const w = moneroWallet
let moneroWalletName = $state('') let moneroWalletName = $state('')
let moneroWalletPass = $state('')
let moneroMnemonic = $state('') let moneroMnemonic = $state('')
let moneroRestoreHeight = $state<number | undefined>(undefined) let moneroRestoreHeight = $state<number | undefined>(undefined)
let moneroSelectedWallet = $state('')
let moneroSelectedWalletPass = $state('')
let moneroUseBiometric = $state(false)
let initStarted = false let initStarted = false
$effect(() => { $effect(() => {
if (w.walletOpen && w.walletName) { if (w.walletOpen && w.walletName) {
navigate('/monero/wallet/:name', { navigate('/monero/wallet/:name', {
params: { params: { name: w.walletName },
name: w.walletName,
},
}) })
} }
}) })
@@ -46,6 +38,12 @@
}) })
} }
}) })
$effect(() => {
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading) {
w.autoUnlock(w.wallets[0])
}
})
</script> </script>
<div class="mx-auto max-w-xl"> <div class="mx-auto max-w-xl">
@@ -101,161 +99,70 @@
</CardContent> </CardContent>
</Card> </Card>
{:else if !w.walletOpen} {:else if !w.walletOpen}
<Card> {#if w.wallets.length > 0}
<CardHeader> <Card>
<CardTitle>Monero Wallet</CardTitle> <CardContent class="flex items-center justify-center py-8">
<CardDescription> <Loader />
{w.wallets.length > 0 <span class="ml-2 text-sm text-muted-foreground">
? `${w.wallets.length} wallet${w.wallets.length > 1 ? 's' : ''} found on disk` Opening wallet...
: 'No existing wallets found'} </span>
</CardDescription> </CardContent>
</CardHeader> </Card>
<CardContent> {:else}
<div class="flex flex-col gap-3"> <Card>
{#if w.wallets.length > 0} <CardHeader>
<p class="text-xs font-medium"> <CardTitle>Monero Wallet</CardTitle>
Select a wallet to open: <CardDescription>
Create a new wallet or restore from seed
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-col gap-3">
<p class="text-xs text-muted-foreground">
Create a new wallet:
</p> </p>
<div class="flex flex-wrap gap-2">
{#each w.wallets as name}
<Button
variant={moneroSelectedWallet === name
? 'default'
: 'outline'}
onclick={() => {
moneroSelectedWallet = name
moneroSelectedWalletPass = ''
}}>
{name}
</Button>
{/each}
</div>
{#if moneroSelectedWallet}
<div class="flex flex-col gap-2">
{#if session.unlocked}
<Button
onclick={() =>
w.autoUnlock(
moneroSelectedWallet,
)}
disabled={w.loading}>
{#if w.loading}
<Loader />
{/if}
Open
</Button>
{:else if w.biometricAvailable}
<Button
onclick={async () => {
const ok = await w.biometricAuth()
if (ok) {
await w.openExistingWallet(
moneroSelectedWallet,
)
}
}}
disabled={w.loading}>
<Fingerprint size={14} />
Open with Touch ID
</Button>
{/if}
<div class="flex gap-2 items-end">
<Input
type="password"
placeholder="Enter password"
bind:value={moneroSelectedWalletPass} />
<Button
onclick={() =>
w.openExistingWallet(
moneroSelectedWallet,
moneroSelectedWalletPass,
)}
disabled={w.loading ||
!moneroSelectedWalletPass}>
Open
</Button>
</div>
</div>
{/if}
<hr class="border-muted" />
{/if}
<p class="text-xs text-muted-foreground">
Or create a new wallet:
</p>
<Input
placeholder="Wallet name"
bind:value={moneroWalletName} />
{#if w.biometricAvailable}
<label
class="flex items-center gap-2 text-xs cursor-pointer">
<input
type="checkbox"
bind:checked={moneroUseBiometric} />
Save password with Touch ID
</label>
{/if}
{#if !moneroUseBiometric}
<Input <Input
type="password" placeholder="Wallet name"
placeholder="Password" bind:value={moneroWalletName} />
bind:value={moneroWalletPass} />
{/if}
<div class="flex gap-2">
<Button <Button
onclick={async () => { onclick={() =>
const pw = moneroUseBiometric w.createWallet(
? crypto.randomUUID()
: moneroWalletPass
await w.createWallet(
moneroWalletName, moneroWalletName,
pw, crypto.randomUUID(),
moneroUseBiometric, true,
) )}
}} disabled={w.loading || !moneroWalletName}>
disabled={w.loading ||
!moneroWalletName ||
(!moneroUseBiometric && !moneroWalletPass)}>
Create Wallet Create Wallet
</Button> </Button>
</div> <hr class="border-muted" />
<hr class="border-muted" /> <p class="text-xs text-muted-foreground">
<p class="text-xs text-muted-foreground"> Restore from seed
Restore from seed </p>
</p> <Textarea
<Textarea placeholder="Enter your Monero seed phrase (16 or 25 words)"
placeholder="Enter your Monero seed phrase (16 or 25 words)" bind:value={moneroMnemonic} />
bind:value={moneroMnemonic} />
<Input
type="number"
placeholder="Restore height (optional)"
bind:value={moneroRestoreHeight} />
{#if !moneroUseBiometric}
<Input <Input
type="password" type="number"
placeholder="Password" placeholder="Restore height (optional)"
bind:value={moneroWalletPass} /> bind:value={moneroRestoreHeight} />
{/if} <Button
<Button onclick={() =>
onclick={async () => { w.restoreWallet(
await w.restoreWallet( moneroWalletName,
moneroWalletName, crypto.randomUUID(),
moneroUseBiometric moneroMnemonic,
? crypto.randomUUID() moneroRestoreHeight,
: moneroWalletPass, true,
moneroMnemonic, )}
moneroRestoreHeight, disabled={w.loading ||
moneroUseBiometric, !moneroWalletName ||
) !moneroMnemonic.trim()}>
}} Restore Wallet
disabled={w.loading || </Button>
!moneroWalletName || </div>
(!moneroUseBiometric && !moneroWalletPass) || </CardContent>
!moneroMnemonic.trim()}> </Card>
Restore Wallet {/if}
</Button>
</div>
</CardContent>
</Card>
{/if} {/if}
{/if} {/if}
</div> </div>
+30 -79
View File
@@ -9,14 +9,10 @@
CardTitle, CardTitle,
} from '$lib/components/ui/card/index.js' } from '$lib/components/ui/card/index.js'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
import ArrowLeft from '@lucide/svelte/icons/arrow-left'
import Trash2 from '@lucide/svelte/icons/trash-2'
import WalletIcon from '@lucide/svelte/icons/wallet'
const w = xrpWallet const w = xrpWallet
let confirm = $state(false) let confirm = $state(false)
let resetting = $state(false) let resetting = $state(false)
let deletingWalletId = $state<string | null>(null)
async function handleReset() { async function handleReset() {
resetting = true resetting = true
@@ -25,84 +21,39 @@
resetting = false resetting = false
confirm = false confirm = false
} }
async function handleDeleteWallet(id: string) {
await w.deleteWallet(id)
deletingWalletId = null
if (w.wallets.length === 0) navigate('/xrp')
}
</script> </script>
<div class="mx-auto max-w-xl"> <div class="mx-auto max-w-xl">
<div class="flex flex-col gap-4"> <Card>
{#if w.wallets.length > 0} <CardHeader>
<Card> <CardTitle>Settings</CardTitle>
<CardHeader> <CardDescription>App preferences and data management</CardDescription>
<CardTitle>Wallets</CardTitle> </CardHeader>
<CardDescription>Manage your XRP wallets</CardDescription> <CardContent class="space-y-4">
</CardHeader> <div>
<CardContent> <p class="font-medium text-xs">Reset App</p>
<div class="flex flex-col gap-2"> <p class="text-muted-foreground text-xs mt-1">
{#each w.wallets as wal} Removes all wallets and seeds from the keychain. You can
<div class="flex items-center gap-3 rounded-md border border-input p-3"> then set up the wallet fresh.
<WalletIcon size={18} class="shrink-0 text-muted-foreground" /> </p>
<div class="flex-1 min-w-0"> <div class="mt-3">
<p class="font-medium text-sm">{wal.name}</p> {#if confirm}
<p class="text-xs text-muted-foreground truncate font-mono"> <div class="flex gap-2 items-center">
{wal.address} <p class="text-xs text-red-500">Are you sure?</p>
</p> <Button size="sm" variant="destructive" onclick={handleReset} disabled={resetting}>
</div> {resetting ? 'Resetting...' : 'Yes, reset everything'}
{#if deletingWalletId === wal.id}
<div class="flex items-center gap-1.5">
<Button size="sm" variant="destructive" onclick={() => handleDeleteWallet(wal.id)}>
Delete
</Button>
<Button size="sm" variant="outline" onclick={() => (deletingWalletId = null)}>
Cancel
</Button>
</div>
{:else}
<Button size="sm" variant="outline" onclick={() => (deletingWalletId = wal.id)}>
<Trash2 size={14} />
</Button>
{/if}
</div>
{/each}
</div>
</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">Reset App</p>
<p class="text-muted-foreground text-xs mt-1">
Removes all XRP wallets and seeds from the keychain. You
can then set up the wallet fresh.
</p>
<div class="mt-3">
{#if confirm}
<div class="flex gap-2 items-center">
<p class="text-xs text-red-500">Are you sure?</p>
<Button size="sm" variant="destructive" onclick={handleReset} disabled={resetting}>
{resetting ? 'Resetting...' : 'Yes, reset everything'}
</Button>
<Button size="sm" variant="outline" onclick={() => (confirm = false)}>
Cancel
</Button>
</div>
{:else}
<Button variant="outline" onclick={() => (confirm = true)}>
Reset App
</Button> </Button>
{/if} <Button size="sm" variant="outline" onclick={() => (confirm = false)}>
</div> Cancel
</Button>
</div>
{:else}
<Button variant="outline" onclick={() => (confirm = true)}>
Reset App
</Button>
{/if}
</div> </div>
</CardContent> </div>
</Card> </CardContent>
</div> </Card>
</div> </div>
+10 -79
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js' import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { session } from '$lib/states/session.svelte.js'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js' import { Input } from '$lib/components/ui/input/index.js'
import { Textarea } from '$lib/components/ui/textarea/index.js' import { Textarea } from '$lib/components/ui/textarea/index.js'
@@ -12,9 +11,6 @@
CardTitle, CardTitle,
} from '$lib/components/ui/card/index.js' } from '$lib/components/ui/card/index.js'
import Fingerprint from '@lucide/svelte/icons/fingerprint' 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 Loader from '$lib/components/loader.svelte'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
@@ -24,7 +20,6 @@
let inputSeed = $state('') let inputSeed = $state('')
let inputPassword = $state('') let inputPassword = $state('')
let inputUnlockPassword = $state('') let inputUnlockPassword = $state('')
let showCreateForm = $state(false)
let newSeed = $state<string | null>(null) let newSeed = $state<string | null>(null)
let initStarted = false let initStarted = false
@@ -108,12 +103,10 @@
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
{:else if w.wallets.length === 0 || showCreateForm} {:else if w.wallets.length === 0}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>Create / Import Wallet</CardTitle>
{showCreateForm ? 'Add Another Wallet' : 'Create / Import Wallet'}
</CardTitle>
<CardDescription> <CardDescription>
Generate a new XRP wallet or import an existing seed Generate a new XRP wallet or import an existing seed
</CardDescription> </CardDescription>
@@ -139,77 +132,20 @@
type="password" type="password"
placeholder="Set a password (optional)" placeholder="Set a password (optional)"
bind:value={inputPassword} /> bind:value={inputPassword} />
<div class="flex gap-2"> <Button
<Button variant="outline"
variant="outline" onclick={handleImport}
onclick={handleImport} disabled={w.loading ||
disabled={w.loading || !inputSeed.trim() ||
!inputSeed.trim() || !inputWalletName.trim()}>
!inputWalletName.trim()}> {w.loading ? 'Importing...' : 'Import Wallet'}
{w.loading ? 'Importing...' : 'Import Wallet'} </Button>
</Button>
{#if showCreateForm}
<Button
variant="outline"
onclick={() => (showCreateForm = false)}>
Cancel
</Button>
{/if}
</div>
</div> </div>
{#if w.error} {#if w.error}
<p class="mt-3 text-xs text-red-500">{w.error}</p> <p class="mt-3 text-xs text-red-500">{w.error}</p>
{/if} {/if}
</CardContent> </CardContent>
</Card> </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 (session.unlocked) {
await w.autoUnlock(wal.id)
} else 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} {:else if w.isLocked}
<Card> <Card>
<CardHeader> <CardHeader>
@@ -254,11 +190,6 @@
</Button> </Button>
</div> </div>
{/if} {/if}
<Button
variant="outline"
onclick={() => w.clearSelection()}>
Pick another wallet
</Button>
</div> </div>
{#if w.error} {#if w.error}
<p class="mt-3 text-xs text-red-500">{w.error}</p> <p class="mt-3 text-xs text-red-500">{w.error}</p>