diff --git a/apps/desktop/electrobun.config.ts b/apps/desktop/electrobun.config.ts index 56c6597..7d2f865 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.4', + version: '0.0.5', }, build: { // Vite builds to dist/, we copy from there diff --git a/apps/desktop/src/bun/lib/monero/wallet.ts b/apps/desktop/src/bun/lib/monero/wallet.ts index d061154..58bf09c 100644 --- a/apps/desktop/src/bun/lib/monero/wallet.ts +++ b/apps/desktop/src/bun/lib/monero/wallet.ts @@ -1,11 +1,7 @@ -import { - MoneroWalletRpc, - MoneroRpcConnection, - connectToWalletRpc, - connectToDaemonRpc, -} from 'monero-ts' +import { MoneroWalletRpc, MoneroRpcConnection, connectToWalletRpc, connectToDaemonRpc } from 'monero-ts' import { getBinaryPath, getWalletDir } from './binary' import { existsSync, readdirSync } from 'fs' +import { log, logError } from '../logger' const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081' @@ -65,20 +61,13 @@ async function waitForRpc( let lastErr = '' while (Date.now() - start < timeoutMs) { try { - await Bun.connect({ - hostname: '127.0.0.1', - port: state.rpcPort, - socket: { - open(s) { - s.end() - }, - close() {}, - data() {}, - error() {}, - }, - }) - console.log(`[monero] RPC ready after ${Date.now() - start}ms`) - return + const conn = rpcConnection(state) + const res = await conn.sendJsonRequest('get_version', {}) + if (res?.result?.version) { + console.log(`[monero] RPC ready after ${Date.now() - start}ms`) + return + } + lastErr = JSON.stringify(res) } catch (e) { lastErr = String(e) } @@ -89,6 +78,28 @@ async function waitForRpc( ) } +async function connectClient( + state: MoneroWalletState, +): Promise { + let lastErr = '' + for (let attempt = 1; attempt <= 3; attempt++) { + try { + const client = (await connectToWalletRpc( + rpcUrl(state), + state.rpcUser, + state.rpcPassword, + )) as MoneroWalletRpc + console.log(`[monero] MoneroWalletRpc connected (attempt ${attempt})`) + return client + } catch (e) { + lastErr = String(e) + console.log(`[monero] connectToWalletRpc attempt ${attempt} failed:`, e) + if (attempt < 3) await new Promise((r) => setTimeout(r, 1000)) + } + } + throw new Error(`Failed to connect to monero-wallet-rpc: ${lastErr}`) +} + export async function start( state: MoneroWalletState, daemonAddress: string = DEFAULT_DAEMON, @@ -169,6 +180,9 @@ export async function start( `[monero] wallet-rpc process exited with code ${exitCode}`, error?.message ?? '', ) + log( + `[monero] wallet-rpc process exited with code ${exitCode} ${error?.message ?? ''}`, + ) if (state.saveTimer) { clearInterval(state.saveTimer) state.saveTimer = null @@ -222,12 +236,9 @@ export async function start( console.log(`[monero] RPC server is ready`) console.log(`[monero] connecting MoneroWalletRpc client...`) - state.wallet = (await connectToWalletRpc( - rpcUrl(state), - state.rpcUser, - state.rpcPassword, - )) as MoneroWalletRpc + state.wallet = await connectClient(state) console.log(`[monero] MoneroWalletRpc connected successfully`) + log(`[monero] wallet-rpc started, RPC ready`) } export async function stop(state: MoneroWalletState) { @@ -241,6 +252,7 @@ export async function stop(state: MoneroWalletState) { if (state.wallet) { await rawRpc(state, 'store') console.log(`[monero] wallet saved before stop`) + log(`[monero] wallet saved before stop`) } } catch (e) { console.log(`[monero] wallet save before stop failed:`, e) @@ -309,9 +321,20 @@ export async function openWallet( password: string, ) { console.log(`[monero] opening wallet: ${name}`) - if (!state.wallet) throw new Error('Wallet RPC not started') - await state.wallet.openWallet(name, password) - console.log(`[monero] wallet opened: ${name}`) + // Self-heal: the client connection may have been lost while the + // wallet-rpc process is still running (e.g. stale-process cleanup). + if (!state.wallet) { + if (!state.process) throw new Error('Wallet RPC not started') + log(`[monero] reconnecting wallet client before open (${name})`) + state.wallet = await connectClient(state) + } + try { + await state.wallet.openWallet(name, password) + log(`[monero] wallet opened: ${name}`) + } catch (e) { + logError(`[monero] wallet open failed (${name})`, e) + throw e + } } export async function closeWallet(state: MoneroWalletState) { diff --git a/apps/desktop/src/bun/rpc/monero/index.ts b/apps/desktop/src/bun/rpc/monero/index.ts index 8fa1bfc..f38532a 100644 --- a/apps/desktop/src/bun/rpc/monero/index.ts +++ b/apps/desktop/src/bun/rpc/monero/index.ts @@ -24,6 +24,7 @@ import { isConnected, } from '../../lib/monero' import type { MoneroWalletState } from '../../lib/monero' +import { log, logError } from '../../lib/logger' export function createMoneroHandlers(state: { manager: MoneroWalletState | null @@ -64,10 +65,12 @@ export function createMoneroHandlers(state: { await walletStart(state.manager, daemonAddress) } catch (e) { console.log('[rpc] moneroStart error:', e) + logError('[rpc] moneroStart error', e) return { running: false, walletOpen: false, connected: false } } const connected = await isConnected(state.manager) console.log('[rpc] moneroStart complete, connected:', connected) + log(`[rpc] moneroStart complete, connected: ${connected}`) return { running: true, walletOpen: false, connected } }, moneroStop: async () => { @@ -129,7 +132,13 @@ export function createMoneroHandlers(state: { console.log('[rpc] moneroOpenWallet:', name) if (!state.manager) throw new Error('Monero wallet RPC not started') - await walletOpen(state.manager, name, password) + try { + await walletOpen(state.manager, name, password) + log(`[rpc] moneroOpenWallet ok: ${name}`) + } catch (e) { + logError(`[rpc] moneroOpenWallet failed (${name})`, e) + throw e + } }, moneroGetBalance: async () => { console.log('[rpc] moneroGetBalance') diff --git a/apps/desktop/src/lib/states/monero-wallet.svelte.ts b/apps/desktop/src/lib/states/monero-wallet.svelte.ts index 53a9638..798572b 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -95,6 +95,7 @@ export const MoneroWallet = () => { if (loginInFlight) return loginInFlight loginInFlight = (async () => { accountType = 'monero' + error = '' beginOpening() try { if (installed && !running) { diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index 1cbfb54..6d8c5b8 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -22,6 +22,7 @@ let xrpBalances = $state>({}) let loadingXrp = $state(true) let loadingXmr = $state(true) + let fetchXrpInFlight = false let feError = $state(null) @@ -70,10 +71,13 @@ }) async function fetchXrp() { + if (fetchXrpInFlight) return + fetchXrpInFlight = true loadingXrp = true const rpc = electrobun.rpc if (!rpc) { loadingXrp = false + fetchXrpInFlight = false return } try { @@ -92,6 +96,7 @@ }), ) loadingXrp = false + fetchXrpInFlight = false } async function fetchXmr() { @@ -111,12 +116,31 @@ onMount(() => { frontendLog('dashboard mounted') moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e)) - xrpWallet.init().then(async () => { - await fetchXrp() - }) + xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e)) fetchXmr() }) + // Wait for the XRP wallet list to load, then fetch balances. + $effect(() => { + if (xrpWallet.ready && xrpWallet.wallets.length > 0) { + fetchXrp() + } + }) + + // Self-heal: if the monero wallet list never got loaded (e.g. the server + // was starting while the dashboard rendered), retry login once. + $effect(() => { + if ( + moneroWallet.ready && + !moneroWallet.walletOpen && + !moneroWallet.opening && + moneroWallet.wallets.length === 0 && + !moneroWallet.error + ) { + void moneroWallet.login() + } + }) + $effect(() => { if (moneroWallet.walletOpen) { fetchXmr() @@ -205,7 +229,7 @@ - {#if loadingXrp} + {#if !xrpWallet.ready || loadingXrp || (xrpWallet.wallets.length > 0 && Object.keys(xrpBalances).length === 0)}
{:else}

{fmtBal(xrpTotalBalance)}