From 399502cf723b3ec3de773c8a7690613d1e6e7308 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 20 Sep 2026 01:30:42 +0800 Subject: [PATCH] Stop a slow chain refresh from showing the wrong wallet balance --- apps/ios/Koins/AppModel.swift | 37 +++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/apps/ios/Koins/AppModel.swift b/apps/ios/Koins/AppModel.swift index 408bc24..c4cb097 100644 --- a/apps/ios/Koins/AppModel.swift +++ b/apps/ios/Koins/AppModel.swift @@ -23,6 +23,9 @@ final class AppModel: ObservableObject { private var refreshTask: Task? private var lastSample: (height: UInt64, date: Date)? private var lastCheckpoint = Date.distantPast + /// Invalidates in-flight refreshes: switching chains (or starting a newer + /// refresh) must stop an older request from writing its result to the UI. + private var refreshToken = UUID() var wallet: WalletRecord? { chain == .xrp ? xrpWallet : xmrWallet @@ -93,6 +96,7 @@ final class AppModel: ObservableObject { func select(_ newChain: Chain) async { chain = newChain + refreshToken = UUID() errorMessage = nil statusMessage = nil balance = Balance(total: "0", unlocked: "", funded: false) @@ -200,14 +204,21 @@ final class AppModel: ObservableObject { func refresh(force: Bool = false) async { guard hasWallet else { return } + let token = UUID() + refreshToken = token + let requestChain = chain errorMessage = nil if chain == .xrp, let wallet = xrpWallet { do { async let balanceCall = XRPWalletService.shared.balance(address: wallet.address) async let txCall = XRPWalletService.shared.transactions(address: wallet.address) - balance = try await balanceCall - transactions = try await txCall + let fetchedBalance = try await balanceCall + let fetchedTransactions = try await txCall + guard !isStale(token, requestChain) else { return } + balance = fetchedBalance + transactions = fetchedTransactions } catch { + guard !isStale(token, requestChain) else { return } errorMessage = error.localizedDescription } // The Monero wallet keeps scanning while XRP is on screen, so keep its @@ -223,22 +234,36 @@ final class AppModel: ObservableObject { do { // Sync progress is cheap and polled every tick; the balance and transfer // list touch the wallet's data files, so they are refreshed less often. - updateSyncStatus(try await MoneroService.shared.syncStatus()) + let status = try await MoneroService.shared.syncStatus() + if !isStale(token, requestChain) { + updateSyncStatus(status) + } refreshTick += 1 await checkpointWalletIfSyncing() if force || refreshTick == 1 || refreshTick % 4 == 0 { async let balanceCall = MoneroService.shared.balance() async let txCall = MoneroService.shared.transactions() async let accountsCall = MoneroService.shared.accounts() - balance = try await balanceCall - transactions = try await txCall - moneroAccounts = try await accountsCall + let fetchedBalance = try await balanceCall + let fetchedTransactions = try await txCall + let fetchedAccounts = try await accountsCall + guard !isStale(token, requestChain) else { return } + balance = fetchedBalance + transactions = fetchedTransactions + moneroAccounts = fetchedAccounts } } catch { + guard !isStale(token, requestChain) else { return } errorMessage = error.localizedDescription } } + /// True when a newer refresh has started or the user moved to another chain + /// while this request was in flight. + private func isStale(_ token: UUID, _ requestChain: Chain) -> Bool { + token != refreshToken || requestChain != chain + } + /// Tracks how fast blocks are being scanned so the UI can show an estimate. private func updateSyncStatus(_ status: SyncStatus) { syncStatus = status