Cache wallet balance and activity so screens show it instantly

This commit is contained in:
2026-09-25 21:58:34 +08:00
parent 2728bf161e
commit b989b0f791
3 changed files with 55 additions and 5 deletions
+26
View File
@@ -41,6 +41,27 @@ final class AppModel: ObservableObject {
var symbol: String { chain.symbol } var symbol: String { chain.symbol }
// MARK: - Cache
/// Restores the last-known state for the chain on screen instantly; the
/// refresh that follows replaces it with live data.
private func showCached(_ forChain: Chain) {
guard let cached = WalletStore.cachedState(forChain) else { return }
balance = cached.balance
transactions = cached.transactions
if let accounts = cached.accounts { moneroAccounts = accounts }
}
private func persistState(for forChain: Chain) {
guard (forChain == .xrp ? xrpWallet : xmrWallet) != nil else { return }
WalletStore.saveCachedState(
ChainCache(balance: balance,
transactions: transactions,
accounts: forChain == .xmr ? moneroAccounts : nil,
savedAt: Date()),
chain: forChain)
}
// MARK: - Bootstrap // MARK: - Bootstrap
func bootstrap() async { func bootstrap() async {
@@ -57,6 +78,7 @@ final class AppModel: ObservableObject {
if xrpWallet != nil || moneroReady { if xrpWallet != nil || moneroReady {
chain = xrpWallet != nil ? .xrp : .xmr chain = xrpWallet != nil ? .xrp : .xmr
} }
showCached(chain)
await refresh(force: true) await refresh(force: true)
startAutoRefresh() startAutoRefresh()
} }
@@ -101,7 +123,9 @@ final class AppModel: ObservableObject {
statusMessage = nil statusMessage = nil
balance = Balance(total: "0", unlocked: "", funded: false) balance = Balance(total: "0", unlocked: "", funded: false)
transactions = [] transactions = []
moneroAccounts = []
syncStatus = nil syncStatus = nil
showCached(newChain)
if newChain == .xmr, let xmrWallet, !moneroOpened { if newChain == .xmr, let xmrWallet, !moneroOpened {
await openMonero(afterRestore: false, wallet: xmrWallet) await openMonero(afterRestore: false, wallet: xmrWallet)
@@ -217,6 +241,7 @@ final class AppModel: ObservableObject {
guard !isStale(token, requestChain) else { return } guard !isStale(token, requestChain) else { return }
balance = fetchedBalance balance = fetchedBalance
transactions = fetchedTransactions transactions = fetchedTransactions
persistState(for: requestChain)
} catch { } catch {
guard !isStale(token, requestChain) else { return } guard !isStale(token, requestChain) else { return }
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
@@ -251,6 +276,7 @@ final class AppModel: ObservableObject {
balance = fetchedBalance balance = fetchedBalance
transactions = fetchedTransactions transactions = fetchedTransactions
moneroAccounts = fetchedAccounts moneroAccounts = fetchedAccounts
persistState(for: requestChain)
} }
} catch { } catch {
guard !isStale(token, requestChain) else { return } guard !isStale(token, requestChain) else { return }
+14 -5
View File
@@ -18,12 +18,12 @@ struct WalletRecord: Codable, Identifiable, Equatable {
var createdAt: Date var createdAt: Date
} }
enum Direction { enum Direction: String, Codable {
case incoming case incoming
case outgoing case outgoing
} }
struct TransactionRecord: Identifiable { struct TransactionRecord: Identifiable, Codable {
let id: String let id: String
let amount: String let amount: String
let timestamp: Date? let timestamp: Date?
@@ -44,12 +44,21 @@ struct TransactionRecord: Identifiable {
} }
struct Balance { struct Balance: Codable {
var total: String var total: String
var unlocked: String var unlocked: String
var funded: Bool var funded: Bool
} }
/// Persisted per chain so a switch or relaunch shows the last-known state
/// instantly while the network catches up.
struct ChainCache: Codable {
var balance: Balance
var transactions: [TransactionRecord]
var accounts: [MoneroAccount]?
var savedAt: Date
}
struct SyncStatus { struct SyncStatus {
var height: UInt64 var height: UInt64
var daemonHeight: UInt64 var daemonHeight: UInt64
@@ -78,7 +87,7 @@ enum TransactionPriority: Int {
} }
} }
struct MoneroSubaddress: Identifiable { struct MoneroSubaddress: Identifiable, Codable {
let accountIndex: Int let accountIndex: Int
let index: Int let index: Int
let address: String let address: String
@@ -90,7 +99,7 @@ struct MoneroSubaddress: Identifiable {
var id: String { "\(accountIndex)/\(index)" } var id: String { "\(accountIndex)/\(index)" }
} }
struct MoneroAccount: Identifiable { struct MoneroAccount: Identifiable, Codable {
let index: Int let index: Int
let address: String let address: String
let balance: String let balance: String
+15
View File
@@ -63,9 +63,24 @@ enum WalletStore {
static func removeWallet(_ chain: Chain) { static func removeWallet(_ chain: Chain) {
defaults.removeObject(forKey: walletKey(chain)) defaults.removeObject(forKey: walletKey(chain))
defaults.removeObject(forKey: cacheKey(chain))
Keychain.remove(secretKey(chain)) Keychain.remove(secretKey(chain))
} }
// MARK: - Cached state (balance, transactions, accounts)
private static func cacheKey(_ chain: Chain) -> String { "koins.cache.\(chain.rawValue)" }
static func cachedState(_ chain: Chain) -> ChainCache? {
guard let data = defaults.data(forKey: cacheKey(chain)) else { return nil }
return try? JSONDecoder().decode(ChainCache.self, from: data)
}
static func saveCachedState(_ state: ChainCache, chain: Chain) {
guard let data = try? JSONEncoder().encode(state) else { return }
defaults.set(data, forKey: cacheKey(chain))
}
static func secret(_ chain: Chain) -> String? { static func secret(_ chain: Chain) -> String? {
Keychain.get(secretKey(chain)) Keychain.get(secretKey(chain))
} }