save monero wallet scan progress

This commit is contained in:
2026-08-02 13:15:41 +08:00
parent c2a405a9e1
commit edb4f6b1f3
2 changed files with 99 additions and 7 deletions
+19
View File
@@ -11,6 +11,7 @@ import { existsSync } from 'fs'
import { getENV } from './lib/get-env' import { getENV } from './lib/get-env'
import { createMoneroHandlers } from './rpc/monero' import { createMoneroHandlers } from './rpc/monero'
import { createXrpHandlers } from './rpc/xrp' import { createXrpHandlers } from './rpc/xrp'
import { stop as walletStop } from './lib/monero'
import type { MoneroWalletState } from './lib/monero' import type { MoneroWalletState } from './lib/monero'
import { canPromptTouchID, promptTouchID } from './lib/biometric' import { canPromptTouchID, promptTouchID } from './lib/biometric'
import { import {
@@ -84,6 +85,24 @@ const moneroState = {
downloading: false, downloading: false,
} }
async function stopWalletServers(): Promise<void> {
if (moneroState.manager) {
const manager = moneroState.manager
moneroState.manager = null
await walletStop(manager)
}
}
// Save the monero wallet and shut down wallet-rpc gracefully on app quit
// (otherwise an orphaned process keeps the scan progress only in memory).
for (const signal of ['SIGTERM', 'SIGINT'] as const) {
process.on(signal, () => {
log(`${signal} received, stopping wallet servers...`)
void stopWalletServers().finally(() => process.exit(0))
setTimeout(() => process.exit(0), 5000).unref?.()
})
}
const rpc = BrowserView.defineRPC<RPC>({ const rpc = BrowserView.defineRPC<RPC>({
maxRequestTime: 120000, maxRequestTime: 120000,
handlers: { handlers: {
+77 -4
View File
@@ -12,6 +12,7 @@ const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
export type MoneroWalletState = { export type MoneroWalletState = {
wallet: MoneroWalletRpc | null wallet: MoneroWalletRpc | null
process: Bun.Subprocess | null process: Bun.Subprocess | null
saveTimer: ReturnType<typeof setInterval> | null
rpcPort: number rpcPort: number
rpcUser: string rpcUser: string
rpcPassword: string rpcPassword: string
@@ -22,6 +23,7 @@ export function createMoneroWalletState(): MoneroWalletState {
return { return {
wallet: null, wallet: null,
process: null, process: null,
saveTimer: null,
rpcPort: 28084, rpcPort: 28084,
rpcUser: 'koins', rpcUser: 'koins',
rpcPassword: 'koins-local-rpc', rpcPassword: 'koins-local-rpc',
@@ -97,18 +99,45 @@ export async function start(
) )
state.daemonAddress = daemonAddress state.daemonAddress = daemonAddress
// Kill any existing monero-wallet-rpc on the port (stale process from dev reloads) // Kill any existing monero-wallet-rpc on the port (stale process from
// dev reloads or an unclean quit). Prefer SIGTERM so a live wallet gets
// a chance to save its scan progress before exiting.
try { try {
const proc = Bun.spawnSync(['lsof', '-ti', `tcp:${state.rpcPort}`], { const proc = Bun.spawnSync(['lsof', '-ti', `tcp:${state.rpcPort}`], {
stdout: 'pipe', stdout: 'pipe',
}) })
const pids = proc.stdout.toString().trim().split('\n').filter(Boolean) const pids = proc.stdout.toString().trim().split('\n').filter(Boolean)
if (pids.length) {
console.log(
`[monero] stale process(es) on port ${state.rpcPort}: ${pids.join(', ')}`,
)
for (const pid of pids) { for (const pid of pids) {
try { try {
process.kill(Number(pid), 'SIGKILL') process.kill(Number(pid), 'SIGTERM')
console.log(`[monero] killed stale process ${pid} on port ${state.rpcPort}`)
} catch {} } catch {}
} }
const deadline = Date.now() + 5000
while (Date.now() < deadline) {
const check = Bun.spawnSync(
['lsof', '-ti', `tcp:${state.rpcPort}`],
{ stdout: 'pipe' },
)
if (!check.stdout.toString().trim()) break
await new Promise((r) => setTimeout(r, 250))
}
const leftover = Bun.spawnSync(
['lsof', '-ti', `tcp:${state.rpcPort}`],
{ stdout: 'pipe' },
)
for (const pid of leftover.stdout.toString().trim().split('\n').filter(Boolean)) {
try {
process.kill(Number(pid), 'SIGKILL')
console.log(
`[monero] killed unresponsive process ${pid} on port ${state.rpcPort}`,
)
} catch {}
}
}
} catch {} } catch {}
console.log(`[monero] starting wallet-rpc process...`) console.log(`[monero] starting wallet-rpc process...`)
@@ -140,10 +169,30 @@ export async function start(
`[monero] wallet-rpc process exited with code ${exitCode}`, `[monero] wallet-rpc process exited with code ${exitCode}`,
error?.message ?? '', error?.message ?? '',
) )
if (state.saveTimer) {
clearInterval(state.saveTimer)
state.saveTimer = null
}
state.wallet = null
state.process = null
}, },
}) })
console.log(`[monero] wallet-rpc pid: ${state.process.pid}`) console.log(`[monero] wallet-rpc pid: ${state.process.pid}`)
// Auto-save the wallet every 60s so scan progress survives an
// unexpected kill (without `store`, the wallet file keeps its old
// refresh height and every restart resyncs from block 0).
state.saveTimer = setInterval(async () => {
try {
if (state.wallet) {
await rawRpc(state, 'store')
console.log(`[monero] wallet auto-saved`)
}
} catch (e) {
console.log(`[monero] wallet auto-save failed:`, e)
}
}, 60000)
const reader = (state.process!.stdout as ReadableStream<Uint8Array>).getReader() const reader = (state.process!.stdout as ReadableStream<Uint8Array>).getReader()
;(async () => { ;(async () => {
while (true) { while (true) {
@@ -183,17 +232,41 @@ export async function start(
export async function stop(state: MoneroWalletState) { export async function stop(state: MoneroWalletState) {
console.log(`[monero] stopping wallet manager...`) console.log(`[monero] stopping wallet manager...`)
if (state.saveTimer) {
clearInterval(state.saveTimer)
state.saveTimer = null
}
// Persist scan progress before shutting down.
try {
if (state.wallet) {
await rawRpc(state, 'store')
console.log(`[monero] wallet saved before stop`)
}
} catch (e) {
console.log(`[monero] wallet save before stop failed:`, e)
}
if (state.process) { if (state.process) {
console.log( console.log(
`[monero] killing wallet-rpc process (pid ${state.process.pid})`, `[monero] sending SIGTERM to wallet-rpc (pid ${state.process.pid})`,
) )
state.process.kill() state.process.kill()
await Promise.race([state.process.exited, sleep(3000)])
if (state.process.exitCode === null) {
console.log(
`[monero] wallet-rpc did not exit gracefully, sending SIGKILL`,
)
state.process.kill('SIGKILL')
}
state.process = null state.process = null
} }
state.wallet = null state.wallet = null
console.log(`[monero] wallet manager stopped`) console.log(`[monero] wallet manager stopped`)
} }
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms))
}
export async function createWallet( export async function createWallet(
state: MoneroWalletState, state: MoneroWalletState,
name: string, name: string,