From 7b4ba84fee5174df0fef1445b60ea566c425e937 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 2 Aug 2026 18:59:31 +0800 Subject: [PATCH] global singleton wallet state with single bootstrap in layout --- .../src/lib/states/monero-wallet.svelte.ts | 30 ++++++++++---- .../src/lib/states/xrp-wallet.svelte.ts | 40 +++++++++++++------ apps/desktop/src/routes/index.svelte | 2 - apps/desktop/src/routes/layout.svelte | 1 + apps/desktop/src/routes/monero/index.svelte | 12 ------ .../desktop/src/routes/monero/settings.svelte | 4 -- apps/desktop/src/routes/xrp/index.svelte | 12 ------ 7 files changed, 52 insertions(+), 49 deletions(-) diff --git a/apps/desktop/src/lib/states/monero-wallet.svelte.ts b/apps/desktop/src/lib/states/monero-wallet.svelte.ts index 6fc312b..269a9cc 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -27,7 +27,7 @@ export const atomicToXmr = (atomic: string): string => { return frac ? `${whole}.${frac}` : `${whole}` } -export const MoneroWallet = () => { +export const createMoneroWallet = () => { let accountType = $state<'monero' | null>(null) let ready = $state(false) let biometricAvailable = $state(false) @@ -105,14 +105,19 @@ export const MoneroWallet = () => { return authed === true } + let initPromise: Promise | null = null const init = async () => { - await checkStatus() - if (installed && !running && !downloading) { - await start() + if (initPromise) return initPromise + initPromise = (async () => { await checkStatus() - } - await checkBiometric() - ready = true + if (installed && !running && !downloading) { + await start() + await checkStatus() + } + await checkBiometric() + ready = true + })() + return initPromise } const login = async () => { @@ -671,4 +676,15 @@ export const MoneroWallet = () => { } } +// Singleton: calling MoneroWallet() always returns the same instance, so +// no component can ever create a second, disconnected wallet state. +type MoneroWalletStore = ReturnType + +let moneroWalletInstance: MoneroWalletStore | null = null + +export const MoneroWallet = (): MoneroWalletStore => { + if (!moneroWalletInstance) moneroWalletInstance = createMoneroWallet() + return moneroWalletInstance +} + export const moneroWallet = MoneroWallet() diff --git a/apps/desktop/src/lib/states/xrp-wallet.svelte.ts b/apps/desktop/src/lib/states/xrp-wallet.svelte.ts index 3595094..315363b 100644 --- a/apps/desktop/src/lib/states/xrp-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/xrp-wallet.svelte.ts @@ -13,7 +13,7 @@ export const xrpNetwork = { explorerAddressUrl: 'https://xrpscan.com/account/', } as const -export const XrpWallet = () => { +export const createXrpWallet = () => { let accountType = $state<'xrp' | null>(null) let ready = $state(false) let biometricAvailable = $state(false) @@ -76,19 +76,24 @@ export const XrpWallet = () => { biometricAvailable = ok === true } + let initPromise: Promise | null = null const init = async () => { - if (!electrobun.rpc) return - const [walletList] = await tryCatch( - electrobun.rpc.request.xrpListWallets({}), - ) - if (walletList) { - wallets = walletList - if (walletList.length > 0 && !currentWalletId) { - currentWalletId = walletList[0].id + if (initPromise) return initPromise + initPromise = (async () => { + if (!electrobun.rpc) return + const [walletList] = await tryCatch( + electrobun.rpc.request.xrpListWallets({}), + ) + if (walletList) { + wallets = walletList + if (walletList.length > 0 && !currentWalletId) { + currentWalletId = walletList[0].id + } } - } - await checkBiometric() - ready = true + await checkBiometric() + ready = true + })() + return initPromise } const login = async () => { @@ -480,4 +485,15 @@ export const XrpWallet = () => { } } +// Singleton: calling XrpWallet() always returns the same instance, so no +// component can ever create a second, disconnected wallet state. +type XrpWalletStore = ReturnType + +let xrpWalletInstance: XrpWalletStore | null = null + +export const XrpWallet = (): XrpWalletStore => { + if (!xrpWalletInstance) xrpWalletInstance = createXrpWallet() + return xrpWalletInstance +} + export const xrpWallet = XrpWallet() diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index c0d90c7..4c4abd7 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -61,8 +61,6 @@ onMount(() => { frontendLog('dashboard mounted') - moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e)) - xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e)) }) // Balances live in the stores so they survive navigation; the effects diff --git a/apps/desktop/src/routes/layout.svelte b/apps/desktop/src/routes/layout.svelte index bdcffc5..894c997 100644 --- a/apps/desktop/src/routes/layout.svelte +++ b/apps/desktop/src/routes/layout.svelte @@ -31,6 +31,7 @@ if (!xrpWallet.ready) await xrpWallet.init() if (!moneroWallet.ready) await moneroWallet.init() await Promise.allSettled([ + xrpWallet.login(), xrpWallet.autoUnlock(), moneroWallet.login(), ]) diff --git a/apps/desktop/src/routes/monero/index.svelte b/apps/desktop/src/routes/monero/index.svelte index d9f8e4f..c9cf376 100644 --- a/apps/desktop/src/routes/monero/index.svelte +++ b/apps/desktop/src/routes/monero/index.svelte @@ -24,8 +24,6 @@ let unlocking = $state(false) let unlockError = $state('') - let initStarted = false - async function unlockWithPassword() { if (unlocking || !walletPassword.trim()) return unlocking = true @@ -48,16 +46,6 @@ } }) - $effect(() => { - if (!initStarted) { - initStarted = true - w.init().then(async () => { - if (w.accountType === 'monero') return - await w.login() - }) - } - }) - $effect(() => { if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) { w.autoUnlock(w.wallets[0]) diff --git a/apps/desktop/src/routes/monero/settings.svelte b/apps/desktop/src/routes/monero/settings.svelte index a5b025d..dac50e0 100644 --- a/apps/desktop/src/routes/monero/settings.svelte +++ b/apps/desktop/src/routes/monero/settings.svelte @@ -15,10 +15,6 @@ const w = moneroWallet let moneroSettingsPw = $state('') - - $effect(() => { - w.init() - })
diff --git a/apps/desktop/src/routes/xrp/index.svelte b/apps/desktop/src/routes/xrp/index.svelte index 8cbd184..ad04490 100644 --- a/apps/desktop/src/routes/xrp/index.svelte +++ b/apps/desktop/src/routes/xrp/index.svelte @@ -22,18 +22,6 @@ let inputUnlockPassword = $state('') let newSeed = $state(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', {