Keep Monero scan progress across app restarts
This commit is contained in:
@@ -21,6 +21,7 @@ final class AppModel: ObservableObject {
|
|||||||
private var refreshTick = 0
|
private var refreshTick = 0
|
||||||
private var refreshTask: Task<Void, Never>?
|
private var refreshTask: Task<Void, Never>?
|
||||||
private var lastSample: (height: UInt64, date: Date)?
|
private var lastSample: (height: UInt64, date: Date)?
|
||||||
|
private var lastCheckpoint = Date.distantPast
|
||||||
|
|
||||||
var wallet: WalletRecord? {
|
var wallet: WalletRecord? {
|
||||||
chain == .xrp ? xrpWallet : xmrWallet
|
chain == .xrp ? xrpWallet : xmrWallet
|
||||||
@@ -212,6 +213,7 @@ final class AppModel: ObservableObject {
|
|||||||
// progress current here too.
|
// progress current here too.
|
||||||
if moneroReady, let status = try? await MoneroService.shared.syncStatus() {
|
if moneroReady, let status = try? await MoneroService.shared.syncStatus() {
|
||||||
updateSyncStatus(status)
|
updateSyncStatus(status)
|
||||||
|
await checkpointWalletIfSyncing()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -222,6 +224,7 @@ final class AppModel: ObservableObject {
|
|||||||
// list touch the wallet's data files, so they are refreshed less often.
|
// list touch the wallet's data files, so they are refreshed less often.
|
||||||
updateSyncStatus(try await MoneroService.shared.syncStatus())
|
updateSyncStatus(try await MoneroService.shared.syncStatus())
|
||||||
refreshTick += 1
|
refreshTick += 1
|
||||||
|
await checkpointWalletIfSyncing()
|
||||||
if force || refreshTick == 1 || refreshTick % 4 == 0 {
|
if force || refreshTick == 1 || refreshTick % 4 == 0 {
|
||||||
async let balanceCall = MoneroService.shared.balance()
|
async let balanceCall = MoneroService.shared.balance()
|
||||||
async let txCall = MoneroService.shared.transactions()
|
async let txCall = MoneroService.shared.transactions()
|
||||||
@@ -334,4 +337,20 @@ final class AppModel: ObservableObject {
|
|||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
statusMessage = nil
|
statusMessage = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when the app leaves the foreground: iOS can suspend or kill it at
|
||||||
|
/// any point, so flush the wallet while there is still time.
|
||||||
|
func persistWallet() async {
|
||||||
|
guard moneroReady else { return }
|
||||||
|
await MoneroService.shared.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// monero-cpp only writes the wallet file from save(), and its sync loop
|
||||||
|
/// never does, so without this a restart re-scans from the beginning.
|
||||||
|
private func checkpointWalletIfSyncing() async {
|
||||||
|
guard moneroReady, syncStatus?.syncing == true else { return }
|
||||||
|
guard Date().timeIntervalSince(lastCheckpoint) >= 60 else { return }
|
||||||
|
lastCheckpoint = Date()
|
||||||
|
await MoneroService.shared.save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +145,16 @@ static void connectDaemon(monero_wallet_full *w) {
|
|||||||
delete g_wallet;
|
delete g_wallet;
|
||||||
g_wallet = nullptr;
|
g_wallet = nullptr;
|
||||||
}
|
}
|
||||||
|
} else if ([op isEqualToString:@"saveWallet"]) {
|
||||||
|
// monero-cpp never persists while syncing, so scan progress survives a
|
||||||
|
// restart only if this is called explicitly. save() pauses the sync
|
||||||
|
// thread while it writes.
|
||||||
|
if (g_wallet) {
|
||||||
|
g_wallet->save();
|
||||||
|
resp[@"saved"] = @YES;
|
||||||
|
} else {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
}
|
||||||
} else if ([op isEqualToString:@"getBalance"]) {
|
} else if ([op isEqualToString:@"getBalance"]) {
|
||||||
if (g_wallet) {
|
if (g_wallet) {
|
||||||
uint64_t balance = 0, unlocked = 0;
|
uint64_t balance = 0, unlocked = 0;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct SeedBox: Identifiable {
|
|||||||
|
|
||||||
struct RootView: View {
|
struct RootView: View {
|
||||||
@EnvironmentObject private var model: AppModel
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@State private var route: Route = .home
|
@State private var route: Route = .home
|
||||||
@State private var seedBox: SeedBox?
|
@State private var seedBox: SeedBox?
|
||||||
|
|
||||||
@@ -54,6 +55,11 @@ struct RootView: View {
|
|||||||
.onChange(of: model.chain) { _ in
|
.onChange(of: model.chain) { _ in
|
||||||
if route == .dashboard { route = .home }
|
if route == .dashboard { route = .home }
|
||||||
}
|
}
|
||||||
|
.onChange(of: scenePhase) { phase in
|
||||||
|
if phase != .active {
|
||||||
|
Task { await model.persistWallet() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +164,26 @@ struct HomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model.chain == .xrp, model.moneroReady, let status = model.syncStatus, status.syncing {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(status.connected ? Theme.success : Theme.danger)
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
Text("Monero is syncing in the background")
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(status.progress * 100))%")
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
SyncProgress(status: status, eta: model.syncEta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
SecondaryButton(title: "Refresh") { Task { await model.refresh(force: true) } }
|
SecondaryButton(title: "Refresh") { Task { await model.refresh(force: true) } }
|
||||||
SecondaryButton(title: "Delete") { showDeleteConfirm = true }
|
SecondaryButton(title: "Delete") { showDeleteConfirm = true }
|
||||||
@@ -183,25 +209,6 @@ struct HomeView: View {
|
|||||||
.foregroundStyle(Theme.muted)
|
.foregroundStyle(Theme.muted)
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.chain == .xrp, model.moneroReady, let status = model.syncStatus, status.syncing {
|
|
||||||
Card {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Circle()
|
|
||||||
.fill(status.connected ? Theme.success : Theme.danger)
|
|
||||||
.frame(width: 7, height: 7)
|
|
||||||
Text("Monero is syncing in the background")
|
|
||||||
.font(.system(size: 12, weight: .medium))
|
|
||||||
.foregroundStyle(Theme.text)
|
|
||||||
Spacer()
|
|
||||||
Text("\(Int(status.progress * 100))%")
|
|
||||||
.font(.system(size: 12, weight: .semibold))
|
|
||||||
.foregroundStyle(Theme.muted)
|
|
||||||
}
|
|
||||||
SyncProgress(status: status, eta: model.syncEta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,11 @@ final class MoneroService {
|
|||||||
_ = try? await invoke(["op": "closeWallet"])
|
_ = try? await invoke(["op": "closeWallet"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the wallet file, which is how scan progress survives a restart.
|
||||||
|
func save() async {
|
||||||
|
_ = try? await invoke(["op": "saveWallet"])
|
||||||
|
}
|
||||||
|
|
||||||
func seed() async throws -> String {
|
func seed() async throws -> String {
|
||||||
let response = try await invoke(["op": "getSeed"])
|
let response = try await invoke(["op": "getSeed"])
|
||||||
return response["mnemonic"] as? String ?? ""
|
return response["mnemonic"] as? String ?? ""
|
||||||
|
|||||||
Reference in New Issue
Block a user