0.0.2 loading states + wallet dialogs + monero unlock flow
This commit is contained in:
@@ -4,7 +4,7 @@ export default {
|
|||||||
app: {
|
app: {
|
||||||
name: 'koins',
|
name: 'koins',
|
||||||
identifier: 'koins.tzgyn.com',
|
identifier: 'koins.tzgyn.com',
|
||||||
version: '0.0.1',
|
version: '0.0.2',
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
// Vite builds to dist/, we copy from there
|
// Vite builds to dist/, we copy from there
|
||||||
|
|||||||
Binary file not shown.
@@ -46,6 +46,21 @@ export const MoneroWallet = () => {
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
let selectedAccountIndex = $state(0)
|
let selectedAccountIndex = $state(0)
|
||||||
|
let opening = $state(false)
|
||||||
|
let openingDepth = 0
|
||||||
|
let passwordRequired = $state(false)
|
||||||
|
let startInFlight: Promise<{ running: boolean; connected: boolean }> | null = null
|
||||||
|
let loginInFlight: Promise<void> | null = null
|
||||||
|
|
||||||
|
const beginOpening = () => {
|
||||||
|
openingDepth += 1
|
||||||
|
opening = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const endOpening = () => {
|
||||||
|
openingDepth = Math.max(0, openingDepth - 1)
|
||||||
|
if (openingDepth === 0) opening = false
|
||||||
|
}
|
||||||
|
|
||||||
const checkBiometric = async () => {
|
const checkBiometric = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
@@ -76,15 +91,33 @@ export const MoneroWallet = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
accountType = 'monero'
|
if (walletOpen) return
|
||||||
if (installed && !running) {
|
if (loginInFlight) return loginInFlight
|
||||||
await start()
|
loginInFlight = (async () => {
|
||||||
await checkStatus()
|
accountType = 'monero'
|
||||||
}
|
beginOpening()
|
||||||
await listWallets()
|
try {
|
||||||
if (wallets.length > 0) {
|
if (installed && !running) {
|
||||||
const pw = await moneroGetStoredPassword(wallets[0])
|
await start()
|
||||||
if (pw) await openExistingWallet(wallets[0], pw)
|
await checkStatus()
|
||||||
|
}
|
||||||
|
await listWallets()
|
||||||
|
if (wallets.length > 0) {
|
||||||
|
const pw = await moneroGetStoredPassword(wallets[0])
|
||||||
|
if (pw) await openExistingWallet(wallets[0], pw)
|
||||||
|
else passwordRequired = true
|
||||||
|
}
|
||||||
|
error = ''
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Failed to open wallet'
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
try {
|
||||||
|
await loginInFlight
|
||||||
|
} finally {
|
||||||
|
loginInFlight = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,13 +176,18 @@ export const MoneroWallet = () => {
|
|||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const pw = password ?? (await moneroGetStoredPassword(name))
|
const pw = password ?? (await moneroGetStoredPassword(name))
|
||||||
if (!pw) throw new Error('Password required')
|
if (!pw) throw new Error('Password required')
|
||||||
await electrobun.rpc.request.moneroOpenWallet({
|
beginOpening()
|
||||||
name,
|
try {
|
||||||
password: pw,
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
})
|
name,
|
||||||
walletName = name
|
password: pw,
|
||||||
walletOpen = true
|
})
|
||||||
await refresh()
|
walletName = name
|
||||||
|
walletOpen = true
|
||||||
|
await refresh()
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkStatus = async () => {
|
const checkStatus = async () => {
|
||||||
@@ -184,9 +222,20 @@ export const MoneroWallet = () => {
|
|||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
const status = await electrobun.rpc.request.moneroStart({})
|
beginOpening()
|
||||||
running = status.running
|
try {
|
||||||
connected = status.connected
|
if (!startInFlight) {
|
||||||
|
startInFlight = electrobun.rpc.request.moneroStart({})
|
||||||
|
const status = await startInFlight
|
||||||
|
running = status.running
|
||||||
|
connected = status.connected
|
||||||
|
} else {
|
||||||
|
await startInFlight
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
startInFlight = null
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = async () => {
|
const stop = async () => {
|
||||||
@@ -195,6 +244,7 @@ export const MoneroWallet = () => {
|
|||||||
running = false
|
running = false
|
||||||
walletOpen = false
|
walletOpen = false
|
||||||
connected = false
|
connected = false
|
||||||
|
passwordRequired = false
|
||||||
address = ''
|
address = ''
|
||||||
balAtomic = '0'
|
balAtomic = '0'
|
||||||
unlockedAtomic = '0'
|
unlockedAtomic = '0'
|
||||||
@@ -209,16 +259,21 @@ export const MoneroWallet = () => {
|
|||||||
storePw?: boolean,
|
storePw?: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroCreateWallet({
|
beginOpening()
|
||||||
name,
|
try {
|
||||||
password,
|
const result = await electrobun.rpc.request.moneroCreateWallet({
|
||||||
})
|
name,
|
||||||
walletName = name
|
password,
|
||||||
walletOpen = true
|
})
|
||||||
address = result.address
|
walletName = name
|
||||||
if (storePw) await moneroStorePassword(name, password)
|
walletOpen = true
|
||||||
await refresh()
|
address = result.address
|
||||||
return result
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
|
await refresh()
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreWallet = async (
|
const restoreWallet = async (
|
||||||
@@ -229,25 +284,35 @@ export const MoneroWallet = () => {
|
|||||||
storePw?: boolean,
|
storePw?: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
beginOpening()
|
||||||
name,
|
try {
|
||||||
password,
|
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
||||||
mnemonic,
|
name,
|
||||||
restoreHeight,
|
password,
|
||||||
})
|
mnemonic,
|
||||||
walletName = name
|
restoreHeight,
|
||||||
walletOpen = true
|
})
|
||||||
address = result.address
|
walletName = name
|
||||||
if (storePw) await moneroStorePassword(name, password)
|
walletOpen = true
|
||||||
await refresh()
|
address = result.address
|
||||||
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
|
await refresh()
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openWallet = async (name: string, password: string) => {
|
const openWallet = async (name: string, password: string) => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
await electrobun.rpc.request.moneroOpenWallet({ name, password })
|
beginOpening()
|
||||||
walletName = name
|
try {
|
||||||
walletOpen = true
|
await electrobun.rpc.request.moneroOpenWallet({ name, password })
|
||||||
await refresh()
|
walletName = name
|
||||||
|
walletOpen = true
|
||||||
|
await refresh()
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoUnlock = async (name?: string) => {
|
const autoUnlock = async (name?: string) => {
|
||||||
@@ -255,15 +320,36 @@ export const MoneroWallet = () => {
|
|||||||
const target = name ?? wallets[0]
|
const target = name ?? wallets[0]
|
||||||
if (!target) return false
|
if (!target) return false
|
||||||
const pw = await moneroGetStoredPassword(target)
|
const pw = await moneroGetStoredPassword(target)
|
||||||
if (!pw) return false
|
if (!pw) {
|
||||||
await electrobun.rpc.request.moneroOpenWallet({
|
passwordRequired = true
|
||||||
name: target,
|
return false
|
||||||
password: pw,
|
}
|
||||||
})
|
beginOpening()
|
||||||
walletName = target
|
try {
|
||||||
walletOpen = true
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
await refresh()
|
name: target,
|
||||||
return true
|
password: pw,
|
||||||
|
})
|
||||||
|
walletName = target
|
||||||
|
walletOpen = true
|
||||||
|
passwordRequired = false
|
||||||
|
error = ''
|
||||||
|
await refresh()
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Failed to open wallet'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlockWallet = async (password: string, remember = false) => {
|
||||||
|
const target = wallets[0]
|
||||||
|
if (!target) return
|
||||||
|
await openWallet(target, password)
|
||||||
|
passwordRequired = false
|
||||||
|
if (remember) await moneroStorePassword(target, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
@@ -404,6 +490,12 @@ export const MoneroWallet = () => {
|
|||||||
get loading() {
|
get loading() {
|
||||||
return loading
|
return loading
|
||||||
},
|
},
|
||||||
|
get opening() {
|
||||||
|
return opening
|
||||||
|
},
|
||||||
|
get passwordRequired() {
|
||||||
|
return passwordRequired
|
||||||
|
},
|
||||||
get error() {
|
get error() {
|
||||||
return error
|
return error
|
||||||
},
|
},
|
||||||
@@ -451,6 +543,7 @@ export const MoneroWallet = () => {
|
|||||||
restoreWallet,
|
restoreWallet,
|
||||||
openWallet,
|
openWallet,
|
||||||
autoUnlock,
|
autoUnlock,
|
||||||
|
unlockWallet,
|
||||||
refresh,
|
refresh,
|
||||||
send,
|
send,
|
||||||
sendAll,
|
sendAll,
|
||||||
|
|||||||
@@ -123,6 +123,17 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (
|
||||||
|
moneroWallet.ready &&
|
||||||
|
!moneroWallet.walletOpen &&
|
||||||
|
!moneroWallet.opening &&
|
||||||
|
!moneroWallet.passwordRequired
|
||||||
|
) {
|
||||||
|
moneroWallet.login()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const handler = (event: ErrorEvent) => {
|
const handler = (event: ErrorEvent) => {
|
||||||
@@ -181,7 +192,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center pt-4 pb-2">
|
<div class="flex flex-col items-center pt-4 pb-2">
|
||||||
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
||||||
{#if loadingXrp || loadingXmr}
|
{#if loadingXrp || loadingXmr || moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen)}
|
||||||
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
||||||
@@ -240,9 +251,7 @@
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{#if loadingXmr && !moneroWallet.walletOpen}
|
{#if moneroWallet.walletOpen}
|
||||||
<div class="h-14 flex items-center"><Loader /></div>
|
|
||||||
{:else if moneroWallet.walletOpen}
|
|
||||||
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
|
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
|
||||||
{#if xmrPrice}
|
{#if xmrPrice}
|
||||||
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
||||||
@@ -252,8 +261,31 @@
|
|||||||
${fmtUsd(parseFloat(xmrPrice) )} / XMR
|
${fmtUsd(parseFloat(xmrPrice) )} / XMR
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0}
|
||||||
|
<p class="text-sm text-muted-foreground py-3">Wallet locked — unlock to view balance</p>
|
||||||
{:else if moneroWallet.wallets.length > 0}
|
{:else if moneroWallet.wallets.length > 0}
|
||||||
<p class="text-sm text-muted-foreground py-3">Open wallet to view balance</p>
|
<div class="h-14 flex items-center gap-2">
|
||||||
|
<Loader />
|
||||||
|
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||||
|
</div>
|
||||||
|
{:else if moneroWallet.opening}
|
||||||
|
<div class="h-14 flex items-center gap-2">
|
||||||
|
<Loader />
|
||||||
|
<p class="text-sm text-muted-foreground">Starting wallet server...</p>
|
||||||
|
</div>
|
||||||
|
{:else if moneroWallet.error}
|
||||||
|
<div class="py-3">
|
||||||
|
<p class="text-xs text-destructive break-words">{moneroWallet.error}</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="mt-2 gap-1.5"
|
||||||
|
onclick={() => moneroWallet.login()}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else if loadingXmr}
|
||||||
|
<div class="h-14 flex items-center"><Loader /></div>
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
onclick={() => navigate('/monero')}
|
onclick={() => navigate('/monero')}
|
||||||
|
|||||||
@@ -19,8 +19,27 @@
|
|||||||
let moneroMnemonic = $state('')
|
let moneroMnemonic = $state('')
|
||||||
let moneroRestoreHeight = $state<number | undefined>(undefined)
|
let moneroRestoreHeight = $state<number | undefined>(undefined)
|
||||||
|
|
||||||
|
let walletPassword = $state('')
|
||||||
|
let rememberPassword = $state(true)
|
||||||
|
let unlocking = $state(false)
|
||||||
|
let unlockError = $state('')
|
||||||
|
|
||||||
let initStarted = false
|
let initStarted = false
|
||||||
|
|
||||||
|
async function unlockWithPassword() {
|
||||||
|
if (unlocking || !walletPassword.trim()) return
|
||||||
|
unlocking = true
|
||||||
|
unlockError = ''
|
||||||
|
try {
|
||||||
|
await w.unlockWallet(walletPassword.trim(), rememberPassword)
|
||||||
|
walletPassword = ''
|
||||||
|
} catch (e) {
|
||||||
|
unlockError = e instanceof Error ? e.message : 'Failed to unlock wallet'
|
||||||
|
} finally {
|
||||||
|
unlocking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (w.walletOpen && w.walletName) {
|
if (w.walletOpen && w.walletName) {
|
||||||
navigate('/monero/wallet/:name', {
|
navigate('/monero/wallet/:name', {
|
||||||
@@ -40,7 +59,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading) {
|
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) {
|
||||||
w.autoUnlock(w.wallets[0])
|
w.autoUnlock(w.wallets[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -66,6 +85,15 @@
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
{:else if w.opening}
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex items-center justify-center gap-2 py-8">
|
||||||
|
<Loader />
|
||||||
|
<span class="text-sm text-muted-foreground">
|
||||||
|
{!w.running ? 'Starting wallet server...' : 'Opening wallet...'}
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
{:else if !w.installed}
|
{:else if !w.installed}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -99,7 +127,48 @@
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{:else if !w.walletOpen}
|
{:else if !w.walletOpen}
|
||||||
{#if w.wallets.length > 0}
|
{#if w.opening}
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex items-center justify-center py-8">
|
||||||
|
<Loader />
|
||||||
|
<span class="ml-2 text-sm text-muted-foreground">
|
||||||
|
Opening wallet...
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{:else if w.passwordRequired && w.wallets.length > 0}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Wallet locked</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the password for {w.wallets[0]} to open it
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Wallet password"
|
||||||
|
bind:value={walletPassword} />
|
||||||
|
<label class="flex items-center gap-2 text-xs">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={rememberPassword}
|
||||||
|
class="h-3.5 w-3.5" />
|
||||||
|
Remember password for auto-unlock
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
onclick={unlockWithPassword}
|
||||||
|
disabled={unlocking || !walletPassword.trim()}>
|
||||||
|
{unlocking ? 'Opening...' : 'Unlock Wallet'}
|
||||||
|
</Button>
|
||||||
|
{#if unlockError}
|
||||||
|
<p class="text-xs text-destructive">{unlockError}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{:else if w.wallets.length > 0}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent class="flex items-center justify-center py-8">
|
<CardContent class="flex items-center justify-center py-8">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
import Send from '@lucide/svelte/icons/send'
|
import Send from '@lucide/svelte/icons/send'
|
||||||
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
||||||
import Plus from '@lucide/svelte/icons/plus'
|
import Plus from '@lucide/svelte/icons/plus'
|
||||||
|
import Users from '@lucide/svelte/icons/users'
|
||||||
|
import Tags from '@lucide/svelte/icons/tags'
|
||||||
import Loader from '$lib/components/loader.svelte'
|
import Loader from '$lib/components/loader.svelte'
|
||||||
import QrCode from '$lib/components/qr.svelte'
|
import QrCode from '$lib/components/qr.svelte'
|
||||||
import { navigate, route } from 'sv-router/generated'
|
import { navigate, route } from 'sv-router/generated'
|
||||||
@@ -97,9 +99,53 @@
|
|||||||
|
|
||||||
let accountDialogOpen = $state(false)
|
let accountDialogOpen = $state(false)
|
||||||
let accountLabel = $state('')
|
let accountLabel = $state('')
|
||||||
|
let accountBusy = $state(false)
|
||||||
|
let accountError = $state('')
|
||||||
let subaddressDialogOpen = $state(false)
|
let subaddressDialogOpen = $state(false)
|
||||||
let subaddressAccountIndex = $state('')
|
let subaddressAccountIndex = $state('')
|
||||||
let subaddressLabel = $state('')
|
let subaddressLabel = $state('')
|
||||||
|
let subaddressBusy = $state(false)
|
||||||
|
let subaddressError = $state('')
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!subaddressAccountIndex && w.accounts.length > 0) {
|
||||||
|
subaddressAccountIndex = String(w.accounts[0].index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleCreateAccount() {
|
||||||
|
if (accountBusy) return
|
||||||
|
accountBusy = true
|
||||||
|
accountError = ''
|
||||||
|
try {
|
||||||
|
await w.createAccount(accountLabel || undefined)
|
||||||
|
accountDialogOpen = false
|
||||||
|
accountLabel = ''
|
||||||
|
} catch (e) {
|
||||||
|
accountError = e instanceof Error ? e.message : 'Failed to create account'
|
||||||
|
} finally {
|
||||||
|
accountBusy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateSubaddress() {
|
||||||
|
if (subaddressBusy || !subaddressAccountIndex) return
|
||||||
|
subaddressBusy = true
|
||||||
|
subaddressError = ''
|
||||||
|
try {
|
||||||
|
await w.createSubaddress(
|
||||||
|
parseInt(subaddressAccountIndex),
|
||||||
|
subaddressLabel || undefined,
|
||||||
|
)
|
||||||
|
subaddressDialogOpen = false
|
||||||
|
subaddressAccountIndex = ''
|
||||||
|
subaddressLabel = ''
|
||||||
|
} catch (e) {
|
||||||
|
subaddressError = e instanceof Error ? e.message : 'Failed to create subaddress'
|
||||||
|
} finally {
|
||||||
|
subaddressBusy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const walletName = $derived(route.params.name)
|
const walletName = $derived(route.params.name)
|
||||||
|
|
||||||
@@ -134,16 +180,27 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!w.walletOpen || w.walletName !== walletName) {
|
if (w.walletOpen && w.walletName === walletName) {
|
||||||
|
w.refresh()
|
||||||
|
electrobun.rpc?.request
|
||||||
|
.fetchMoneroPrice({})
|
||||||
|
.then((p) => (moneroPrice = p))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!w.opening && (!w.walletOpen || w.walletName !== walletName)) {
|
||||||
navigate('/monero')
|
navigate('/monero')
|
||||||
}
|
}
|
||||||
w.refresh()
|
|
||||||
electrobun.rpc?.request
|
|
||||||
.fetchMoneroPrice({})
|
|
||||||
.then((p) => (moneroPrice = p))
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if !w.walletOpen || w.walletName !== walletName}
|
||||||
|
<div class="flex flex-col items-center justify-center gap-3 py-24">
|
||||||
|
<Loader />
|
||||||
|
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-col items-center py-2">
|
<div class="flex flex-col items-center py-2">
|
||||||
{#if w.loading}
|
{#if w.loading}
|
||||||
@@ -340,14 +397,44 @@
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Create Account</Dialog.Title>
|
<div class="flex items-center gap-3">
|
||||||
<Dialog.Description>Optional label for the new account</Dialog.Description>
|
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||||
|
<Users size={16} class="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Dialog.Title>Create Account</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
New account with its own balance and addresses
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
<div class="px-6 pb-4 space-y-3">
|
<div class="px-6 pb-4 space-y-3">
|
||||||
<Input placeholder="Account label (optional)" bind:value={accountLabel} />
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-xs font-medium" for="account-label">Label</label>
|
||||||
|
<Input id="account-label" placeholder="Savings" bind:value={accountLabel} />
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Optional — shown next to the account in your wallet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if accountError}
|
||||||
|
<p class="text-xs text-destructive">{accountError}</p>
|
||||||
|
{/if}
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button variant="outline" size="sm" onclick={() => { accountDialogOpen = false; accountLabel = '' }}>Cancel</Button>
|
<Button
|
||||||
<Button size="sm" onclick={async () => { await w.createAccount(accountLabel || undefined); accountDialogOpen = false; accountLabel = '' }}>Create</Button>
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={accountBusy}
|
||||||
|
onclick={() => {
|
||||||
|
accountDialogOpen = false
|
||||||
|
accountLabel = ''
|
||||||
|
accountError = ''
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" disabled={accountBusy} onclick={handleCreateAccount}>
|
||||||
|
{accountBusy ? 'Creating...' : 'Create'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
@@ -360,29 +447,66 @@
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Create Subaddress</Dialog.Title>
|
<div class="flex items-center gap-3">
|
||||||
<Dialog.Description>Account index and optional label</Dialog.Description>
|
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||||
|
<Tags size={16} class="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Dialog.Title>Create Subaddress</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Receiving address tied to an account
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
<div class="px-6 pb-4 space-y-3">
|
<div class="px-6 pb-4 space-y-3">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label class="text-xs font-medium">Account</label>
|
<label class="text-xs font-medium" for="subaddress-account">Account</label>
|
||||||
<Select type="single" bind:value={subaddressAccountIndex}>
|
<Select type="single" bind:value={subaddressAccountIndex}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{subaddressAccountIndex ? `Account ${subaddressAccountIndex}` : 'Select account'}
|
Account {subaddressAccountIndex}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{#each w.accounts as acct}
|
{#each w.accounts as acct}
|
||||||
<SelectItem value={String(acct.index)}>
|
<SelectItem value={String(acct.index)}>
|
||||||
Account {acct.index}{acct.label ? ` - ${acct.label}` : ''}
|
Account {acct.index}{acct.label ? ` - ${acct.label}` : ''} ({atomicToXmr(acct.unlockedBalance)} XMR)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{/each}
|
{/each}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Where the subaddress belongs
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input placeholder="Subaddress label (optional)" bind:value={subaddressLabel} />
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-xs font-medium" for="subaddress-label">Label</label>
|
||||||
|
<Input id="subaddress-label" placeholder="Donations" bind:value={subaddressLabel} />
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Optional — helps you track who sent you funds
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if subaddressError}
|
||||||
|
<p class="text-xs text-destructive">{subaddressError}</p>
|
||||||
|
{/if}
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button variant="outline" size="sm" onclick={() => { subaddressDialogOpen = false; subaddressAccountIndex = ''; subaddressLabel = '' }}>Cancel</Button>
|
<Button
|
||||||
<Button size="sm" disabled={!subaddressAccountIndex} onclick={async () => { await w.createSubaddress(parseInt(subaddressAccountIndex), subaddressLabel || undefined); subaddressDialogOpen = false; subaddressAccountIndex = ''; subaddressLabel = '' }}>Create</Button>
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={subaddressBusy}
|
||||||
|
onclick={() => {
|
||||||
|
subaddressDialogOpen = false
|
||||||
|
subaddressAccountIndex = ''
|
||||||
|
subaddressLabel = ''
|
||||||
|
subaddressError = ''
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled={subaddressBusy || !subaddressAccountIndex}
|
||||||
|
onclick={handleCreateSubaddress}>
|
||||||
|
{subaddressBusy ? 'Creating...' : 'Create'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
@@ -456,3 +580,4 @@
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user