From c9e492ad957531b6136b7111973e5b777166b742 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 2 Aug 2026 12:15:55 +0800 Subject: [PATCH] 0.0.2 loading states + wallet dialogs + monero unlock flow --- apps/desktop/electrobun.config.ts | 2 +- apps/desktop/src/bun/native/biometric-helper | Bin 72832 -> 72832 bytes .../src/lib/states/monero-wallet.svelte.ts | 199 +++++++++++++----- apps/desktop/src/routes/index.svelte | 42 +++- apps/desktop/src/routes/monero/index.svelte | 73 ++++++- .../src/routes/monero/wallet/[name].svelte | 163 ++++++++++++-- 6 files changed, 399 insertions(+), 80 deletions(-) diff --git a/apps/desktop/electrobun.config.ts b/apps/desktop/electrobun.config.ts index a7e86a3..b2593f8 100644 --- a/apps/desktop/electrobun.config.ts +++ b/apps/desktop/electrobun.config.ts @@ -4,7 +4,7 @@ export default { app: { name: 'koins', identifier: 'koins.tzgyn.com', - version: '0.0.1', + version: '0.0.2', }, build: { // Vite builds to dist/, we copy from there diff --git a/apps/desktop/src/bun/native/biometric-helper b/apps/desktop/src/bun/native/biometric-helper index bc745883c58e149ca9fbf4127ad34b9f09daaa42..188023120bb0ce2494c10523ca13196df649dae6 100755 GIT binary patch delta 811 zcmV+`1JwM0xdec@1h6O60cf)*)DHpySfe=B4+Tjuf)quQJ>|LvGB7kTGcmLM<*@;O zpYCO9M(m91_NVlX`;#yZs5@_B*grHT-#ba1OHZm2@ImyvUA>u3EYFppRx{sPE*s0< zRN~!NGF86GY`PcU!a5?aE4t9?85}NX>5q^X5nD>uGD}KfDs8Zm2r!q%FVh(>5Vuu$ zI>;_`V4%2vHQYr#xxd0QJ2M@rE98uSk16-$Yx%Lb5xk<3wXpii)Pj6Tdx#*g&^?1% zb~0M7ZeyhZu6;Fb!hbfP1402CJJPLm?nE<;&ZEv-iCQ^uI1?>4y*O)>byRl|kAZQ|$9%-&DZ)LKS%;RX$V^RS_U z5YRA!5X=S(Duzgg_YDCF5CRS{f)Kkff)KX`2`Yw2hW8Bt2Lhmi5U4PM5TpVD12GFQ z2?hl#4g&%j1pqL51`8^NNQU&>-iPOU3#_&}?9S?U1b9BslN!4**(%X)6A2f^EIRXe^6y8|79~J!= z4>B+^HZV9aGB7kTGch?@FarVs0RjjnsOqeo>RuFEiy1PF*7kaS}-F92`Yw2 zhW8Bt2?Q|~1QZZZKrgk*bi5$I6yE$<#?M{9VAqpF_&|Rn$(d%;AHzv)iBxdBMnw3c zsm|Q%R!Rz0Zv%TInX&!q1Ft3cb)!Hy$~PMc`uKs_D>pq6a>MstQAg9CVfhcWp5A)L zClcQ|>5+Wqu@YDIrYFS|hf7V;Bl2oYz965Audfd+rAkwjh*g4Qeyz{_bbZ*WKOTW= z_oSlfsak&ndJ`Fvqer-F;pCEz;g_x?F(etWEQp~}%)9Fbri;!{fSux}+3Y;FnyR~3 z2^at`ZklY_+V|6^AARbqQOTLK#-puy)TAD8Dl|LvF)=eWIW)8V<*@;O z2ApYcVc8duz&Imy(?VW_Xi%f3tHt*V~4-XUa9+>H)osV{;4sAi3$;;sF?v;qiF2?@s{U|w?= z4>B+^HZV9aF)=eWIW#$1FarVs0RjmChYyk7(ufZ>vuf$94lOY;Gc`FhIa)9y1_>&L zNQU#76cT0FwG5{#@z}l?{@l=Lij*`QeP;v)dhR&0v&JEHfRQE zFj2&R?DVbeu21HNNkw>$8C;+SB`{Swkbp#dfGGxx{ka=GQiqsboUiGyDe83NAkU&# zPshC62cnTf-mPuu;~l!7qTTTqRL0+m2uaQ*4_ALI2=wG$EW-{C`$2`2HwlMq0$Q^5 zc%Fux^Qj_#NVl4;lz|XCwXfWIF8!6X{NTLaJ`OW-u~Tetsi0Z{y>rBHe`PB~CZDsq z?DX_zCJXvqQqR@KQ^UIVM0fGf#?S85!QUt_F=QPN&Z}A;Y3g~snx8uG&It>1eSOXM qgOe1%2IJIX#OVrTm+H%Vq21JU<=_a-gbJ)g08hUHX7;mT6a6Ci7;5(b diff --git a/apps/desktop/src/lib/states/monero-wallet.svelte.ts b/apps/desktop/src/lib/states/monero-wallet.svelte.ts index 9a5f829..e9bb82d 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -46,6 +46,21 @@ export const MoneroWallet = () => { let loading = $state(false) let error = $state('') 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 | null = null + + const beginOpening = () => { + openingDepth += 1 + opening = true + } + + const endOpening = () => { + openingDepth = Math.max(0, openingDepth - 1) + if (openingDepth === 0) opening = false + } const checkBiometric = async () => { if (!electrobun.rpc) return @@ -76,15 +91,33 @@ export const MoneroWallet = () => { } const login = async () => { - accountType = 'monero' - if (installed && !running) { - await start() - await checkStatus() - } - await listWallets() - if (wallets.length > 0) { - const pw = await moneroGetStoredPassword(wallets[0]) - if (pw) await openExistingWallet(wallets[0], pw) + if (walletOpen) return + if (loginInFlight) return loginInFlight + loginInFlight = (async () => { + accountType = 'monero' + beginOpening() + try { + if (installed && !running) { + await start() + 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') const pw = password ?? (await moneroGetStoredPassword(name)) if (!pw) throw new Error('Password required') - await electrobun.rpc.request.moneroOpenWallet({ - name, - password: pw, - }) - walletName = name - walletOpen = true - await refresh() + beginOpening() + try { + await electrobun.rpc.request.moneroOpenWallet({ + name, + password: pw, + }) + walletName = name + walletOpen = true + await refresh() + } finally { + endOpening() + } } const checkStatus = async () => { @@ -184,9 +222,20 @@ export const MoneroWallet = () => { const start = async () => { if (!electrobun.rpc) return - const status = await electrobun.rpc.request.moneroStart({}) - running = status.running - connected = status.connected + beginOpening() + try { + 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 () => { @@ -195,6 +244,7 @@ export const MoneroWallet = () => { running = false walletOpen = false connected = false + passwordRequired = false address = '' balAtomic = '0' unlockedAtomic = '0' @@ -209,16 +259,21 @@ export const MoneroWallet = () => { storePw?: boolean, ) => { if (!electrobun.rpc) throw new Error('RPC not available') - const result = await electrobun.rpc.request.moneroCreateWallet({ - name, - password, - }) - walletName = name - walletOpen = true - address = result.address - if (storePw) await moneroStorePassword(name, password) - await refresh() - return result + beginOpening() + try { + const result = await electrobun.rpc.request.moneroCreateWallet({ + name, + password, + }) + walletName = name + walletOpen = true + address = result.address + if (storePw) await moneroStorePassword(name, password) + await refresh() + return result + } finally { + endOpening() + } } const restoreWallet = async ( @@ -229,25 +284,35 @@ export const MoneroWallet = () => { storePw?: boolean, ) => { if (!electrobun.rpc) throw new Error('RPC not available') - const result = await electrobun.rpc.request.moneroRestoreWallet({ - name, - password, - mnemonic, - restoreHeight, - }) - walletName = name - walletOpen = true - address = result.address - if (storePw) await moneroStorePassword(name, password) - await refresh() + beginOpening() + try { + const result = await electrobun.rpc.request.moneroRestoreWallet({ + name, + password, + mnemonic, + restoreHeight, + }) + walletName = name + walletOpen = true + address = result.address + if (storePw) await moneroStorePassword(name, password) + await refresh() + } finally { + endOpening() + } } const openWallet = async (name: string, password: string) => { if (!electrobun.rpc) return - await electrobun.rpc.request.moneroOpenWallet({ name, password }) - walletName = name - walletOpen = true - await refresh() + beginOpening() + try { + await electrobun.rpc.request.moneroOpenWallet({ name, password }) + walletName = name + walletOpen = true + await refresh() + } finally { + endOpening() + } } const autoUnlock = async (name?: string) => { @@ -255,15 +320,36 @@ export const MoneroWallet = () => { const target = name ?? wallets[0] if (!target) return false const pw = await moneroGetStoredPassword(target) - if (!pw) return false - await electrobun.rpc.request.moneroOpenWallet({ - name: target, - password: pw, - }) - walletName = target - walletOpen = true - await refresh() - return true + if (!pw) { + passwordRequired = true + return false + } + beginOpening() + try { + await electrobun.rpc.request.moneroOpenWallet({ + name: target, + 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 () => { @@ -404,6 +490,12 @@ export const MoneroWallet = () => { get loading() { return loading }, + get opening() { + return opening + }, + get passwordRequired() { + return passwordRequired + }, get error() { return error }, @@ -451,6 +543,7 @@ export const MoneroWallet = () => { restoreWallet, openWallet, autoUnlock, + unlockWallet, refresh, send, sendAll, diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index 469cce9..43a37ea 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -123,6 +123,17 @@ } }) + $effect(() => { + if ( + moneroWallet.ready && + !moneroWallet.walletOpen && + !moneroWallet.opening && + !moneroWallet.passwordRequired + ) { + moneroWallet.login() + } + }) + $effect(() => { if (typeof window !== 'undefined') { const handler = (event: ErrorEvent) => { @@ -181,7 +192,7 @@ {:else}

Total Portfolio Value

- {#if loadingXrp || loadingXmr} + {#if loadingXrp || loadingXmr || moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen)}
{:else}

@@ -240,9 +251,7 @@ - {#if loadingXmr && !moneroWallet.walletOpen} -

- {:else if moneroWallet.walletOpen} + {#if moneroWallet.walletOpen}

{fmtBal(xmrBalance, 4)}

{#if xmrPrice}

@@ -252,8 +261,31 @@ ${fmtUsd(parseFloat(xmrPrice) )} / XMR

{/if} + {:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0} +

Wallet locked — unlock to view balance

{:else if moneroWallet.wallets.length > 0} -

Open wallet to view balance

+
+ +

Opening wallet...

+
+ {:else if moneroWallet.opening} +
+ +

Starting wallet server...

+
+ {:else if moneroWallet.error} +
+

{moneroWallet.error}

+ +
+ {:else if loadingXmr} +
{:else} + {#if unlockError} +

{unlockError}

+ {/if} +
+ + + {:else if w.wallets.length > 0} diff --git a/apps/desktop/src/routes/monero/wallet/[name].svelte b/apps/desktop/src/routes/monero/wallet/[name].svelte index d8f2d7a..c31f185 100644 --- a/apps/desktop/src/routes/monero/wallet/[name].svelte +++ b/apps/desktop/src/routes/monero/wallet/[name].svelte @@ -24,6 +24,8 @@ import Send from '@lucide/svelte/icons/send' import QrCodeIcon from '@lucide/svelte/icons/qr-code' 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 QrCode from '$lib/components/qr.svelte' import { navigate, route } from 'sv-router/generated' @@ -97,9 +99,53 @@ let accountDialogOpen = $state(false) let accountLabel = $state('') + let accountBusy = $state(false) + let accountError = $state('') let subaddressDialogOpen = $state(false) let subaddressAccountIndex = $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) @@ -134,16 +180,27 @@ }) 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') } - w.refresh() - electrobun.rpc?.request - .fetchMoneroPrice({}) - .then((p) => (moneroPrice = p)) }) +{#if !w.walletOpen || w.walletName !== walletName} +
+ +

Opening wallet...

+
+{:else}
{#if w.loading} @@ -340,14 +397,44 @@ - Create Account - Optional label for the new account +
+
+ +
+
+ Create Account + + New account with its own balance and addresses + +
+
- +
+ + +

+ Optional — shown next to the account in your wallet +

+
+ {#if accountError} +

{accountError}

+ {/if}
- - + +
@@ -360,29 +447,66 @@ - Create Subaddress - Account index and optional label +
+
+ +
+
+ Create Subaddress + + Receiving address tied to an account + +
+
- + +

+ Where the subaddress belongs +

- +
+ + +

+ Optional — helps you track who sent you funds +

+
+ {#if subaddressError} +

{subaddressError}

+ {/if}
- - + +
@@ -455,4 +579,5 @@
-
\ No newline at end of file + +{/if} \ No newline at end of file