Cache wallet balance and activity so screens show it instantly
This commit is contained in:
@@ -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 }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user