228 lines
9.1 KiB
Swift
228 lines
9.1 KiB
Swift
//
|
|
// WalletBalancesStore.swift
|
|
// Zashi
|
|
//
|
|
// Created by Lukáš Korba on 04-02-2024
|
|
//
|
|
|
|
import Foundation
|
|
import ComposableArchitecture
|
|
|
|
import ExchangeRate
|
|
import Models
|
|
import SDKSynchronizer
|
|
import Utils
|
|
import ZcashLightClientKit
|
|
import ZcashSDKEnvironment
|
|
import UserPreferencesStorage
|
|
|
|
@Reducer
|
|
public struct WalletBalances {
|
|
private let CancelStateId = UUID()
|
|
private let CancelRateId = UUID()
|
|
|
|
@ObservableState
|
|
public struct State: Equatable {
|
|
public var autoShieldingThreshold: Zatoshi = .zero
|
|
@Shared(.inMemory(.exchangeRate)) public var currencyConversion: CurrencyConversion? = nil
|
|
public var fiatCurrencyResult: FiatCurrencyResult?
|
|
public var isAvailableBalanceTappable = true
|
|
public var isExchangeRateFeatureOn = false
|
|
public var isExchangeRateRefreshEnabled = false
|
|
public var isExchangeRateStale = false
|
|
public var migratingDatabase = false
|
|
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
|
|
public var shieldedBalance: Zatoshi
|
|
public var shieldedWithPendingBalance: Zatoshi
|
|
public var spendability: Spendability = .everything
|
|
public var totalBalance: Zatoshi
|
|
public var transparentBalance: Zatoshi
|
|
|
|
public var isExchangeRateUSDInFlight: Bool {
|
|
fiatCurrencyResult?.state == .fetching
|
|
}
|
|
|
|
public var isProcessingZeroAvailableBalance: Bool {
|
|
if shieldedBalance.amount == 0 && transparentBalance.amount > autoShieldingThreshold.amount {
|
|
return false
|
|
}
|
|
|
|
return totalBalance.amount != shieldedBalance.amount && shieldedBalance.amount == 0
|
|
}
|
|
|
|
public var currencyValue: String {
|
|
currencyConversion?.convert(totalBalance) ?? ""
|
|
}
|
|
|
|
public init(
|
|
fiatCurrencyResult: FiatCurrencyResult? = nil,
|
|
isAvailableBalanceTappable: Bool = true,
|
|
isExchangeRateFeatureOn: Bool = false,
|
|
isExchangeRateRefreshEnabled: Bool = false,
|
|
isExchangeRateStale: Bool = false,
|
|
migratingDatabase: Bool = false,
|
|
shieldedBalance: Zatoshi = .zero,
|
|
shieldedWithPendingBalance: Zatoshi = .zero,
|
|
totalBalance: Zatoshi = .zero,
|
|
transparentBalance: Zatoshi = .zero
|
|
) {
|
|
self.fiatCurrencyResult = fiatCurrencyResult
|
|
self.isAvailableBalanceTappable = isAvailableBalanceTappable
|
|
self.isExchangeRateFeatureOn = isExchangeRateFeatureOn
|
|
self.isExchangeRateRefreshEnabled = isExchangeRateRefreshEnabled
|
|
self.isExchangeRateStale = isExchangeRateStale
|
|
self.migratingDatabase = migratingDatabase
|
|
self.shieldedBalance = shieldedBalance
|
|
self.shieldedWithPendingBalance = shieldedWithPendingBalance
|
|
self.totalBalance = totalBalance
|
|
self.transparentBalance = transparentBalance
|
|
}
|
|
}
|
|
|
|
public enum Action: Equatable {
|
|
case availableBalanceTapped
|
|
case balanceUpdated(AccountBalance?)
|
|
case debugMenuStartup
|
|
case exchangeRateRefreshTapped
|
|
case exchangeRateEvent(ExchangeRateClient.EchangeRateEvent)
|
|
case onAppear
|
|
case onDisappear
|
|
case synchronizerStateChanged(RedactableSynchronizerState)
|
|
case updateBalances
|
|
}
|
|
|
|
@Dependency(\.exchangeRate) var exchangeRate
|
|
@Dependency(\.mainQueue) var mainQueue
|
|
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
|
|
@Dependency(\.userStoredPreferences) var userStoredPreferences
|
|
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
|
|
|
|
public init() { }
|
|
|
|
public var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .onAppear:
|
|
state.autoShieldingThreshold = zcashSDKEnvironment.shieldingThreshold
|
|
if let exchangeRate = userStoredPreferences.exchangeRate(), exchangeRate.automatic {
|
|
state.isExchangeRateFeatureOn = true
|
|
} else {
|
|
state.isExchangeRateFeatureOn = false
|
|
}
|
|
return .merge(
|
|
.send(.updateBalances),
|
|
.publisher {
|
|
sdkSynchronizer.stateStream()
|
|
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
|
.map { $0.redacted }
|
|
.map(Action.synchronizerStateChanged)
|
|
}
|
|
.cancellable(id: CancelStateId, cancelInFlight: true),
|
|
.publisher {
|
|
exchangeRate.exchangeRateEventStream()
|
|
.map(Action.exchangeRateEvent)
|
|
.receive(on: mainQueue)
|
|
}
|
|
.cancellable(id: CancelRateId, cancelInFlight: true)
|
|
)
|
|
|
|
case .onDisappear:
|
|
return .merge(
|
|
.cancel(id: CancelStateId),
|
|
.cancel(id: CancelRateId)
|
|
)
|
|
|
|
case .availableBalanceTapped:
|
|
return .none
|
|
|
|
case .exchangeRateRefreshTapped:
|
|
if !state.isExchangeRateStale {
|
|
exchangeRate.refreshExchangeRateUSD()
|
|
}
|
|
return .none
|
|
|
|
case .exchangeRateEvent(let result):
|
|
switch result {
|
|
case .value(let rate):
|
|
guard let rate else {
|
|
return .none
|
|
}
|
|
|
|
state.fiatCurrencyResult = rate
|
|
state.$currencyConversion.withLock {
|
|
$0 = CurrencyConversion(.usd, ratio: rate.rate.doubleValue, timestamp: rate.date.timeIntervalSince1970)
|
|
}
|
|
state.isExchangeRateRefreshEnabled = false
|
|
state.isExchangeRateStale = false
|
|
case .refreshEnable(let rate):
|
|
guard let rate else {
|
|
return .none
|
|
}
|
|
|
|
state.fiatCurrencyResult = rate
|
|
state.$currencyConversion.withLock {
|
|
$0 = CurrencyConversion(.usd, ratio: rate.rate.doubleValue, timestamp: rate.date.timeIntervalSince1970)
|
|
}
|
|
state.isExchangeRateRefreshEnabled = true
|
|
state.isExchangeRateStale = false
|
|
case .stale:
|
|
state.$currencyConversion.withLock {
|
|
$0 = nil
|
|
}
|
|
state.isExchangeRateStale = true
|
|
break
|
|
}
|
|
|
|
return .none
|
|
|
|
case .updateBalances:
|
|
guard let account = state.selectedWalletAccount else {
|
|
return .none
|
|
}
|
|
return .run { send in
|
|
if let accountBalance = try? await sdkSynchronizer.getAccountsBalances()[account.id] {
|
|
await send(.balanceUpdated(accountBalance))
|
|
} else if let accountBalance = sdkSynchronizer.latestState().accountsBalances[account.id] {
|
|
await send(.balanceUpdated(accountBalance))
|
|
}
|
|
}
|
|
|
|
case .balanceUpdated(let accountBalance):
|
|
state.shieldedBalance = (accountBalance?.saplingBalance.spendableValue ?? .zero) + (accountBalance?.orchardBalance.spendableValue ?? .zero)
|
|
state.shieldedWithPendingBalance = (accountBalance?.saplingBalance.total() ?? .zero) + (accountBalance?.orchardBalance.total() ?? .zero)
|
|
state.transparentBalance = accountBalance?.unshielded ?? .zero
|
|
state.totalBalance = state.shieldedWithPendingBalance + state.transparentBalance
|
|
|
|
let everythingCondition = state.shieldedBalance.amount > 0 && ((state.shieldedBalance == state.totalBalance)
|
|
|| (state.transparentBalance < zcashSDKEnvironment.shieldingThreshold && state.shieldedBalance == state.totalBalance - state.transparentBalance))
|
|
|
|
// spendability
|
|
if state.isProcessingZeroAvailableBalance {
|
|
state.spendability = .nothing
|
|
} else if everythingCondition {
|
|
state.spendability = .everything
|
|
} else {
|
|
state.spendability = .something
|
|
}
|
|
return .none
|
|
|
|
case .debugMenuStartup:
|
|
return .none
|
|
|
|
case .synchronizerStateChanged(let latestState):
|
|
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.data.syncStatus)
|
|
|
|
if snapshot.syncStatus != .unprepared {
|
|
state.migratingDatabase = false
|
|
}
|
|
|
|
guard let account = state.selectedWalletAccount else {
|
|
return .none
|
|
}
|
|
|
|
return .send(.balanceUpdated(latestState.data.accountsBalances[account.id]))
|
|
}
|
|
}
|
|
}
|
|
}
|