save monero wallet scan progress
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
@@ -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,17 +99,44 @@ 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)
|
||||||
for (const pid of pids) {
|
if (pids.length) {
|
||||||
try {
|
console.log(
|
||||||
process.kill(Number(pid), 'SIGKILL')
|
`[monero] stale process(es) on port ${state.rpcPort}: ${pids.join(', ')}`,
|
||||||
console.log(`[monero] killed stale process ${pid} on port ${state.rpcPort}`)
|
)
|
||||||
} catch {}
|
for (const pid of pids) {
|
||||||
|
try {
|
||||||
|
process.kill(Number(pid), 'SIGTERM')
|
||||||
|
} 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 {}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user