secant-ios-wallet/modules/Sources/Features/BalanceBreakdown/BalancesStore.swift

214 lines
8.3 KiB
Swift

//
// BalancesStore.swift
// Zashi
//
// Created by Lukáš Korba on 04.08.2022.
//
import SwiftUI
import ComposableArchitecture
import ZcashLightClientKit
import DerivationTool
import MnemonicClient
import NumberFormatter
import Utils
import Generated
import WalletStorage
import SDKSynchronizer
import Models
import ZcashSDKEnvironment
import ShieldingProcessor
@Reducer
public struct Balances {
@ObservableState
public struct State: Equatable {
public var stateStreamCancelId = UUID()
public var shieldingProcessorCancelId = UUID()
public var autoShieldingThreshold: Zatoshi
public var changePending: Zatoshi
public var isShielding: Bool
public var pendingTransactions: Zatoshi
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
public var shieldedBalance: Zatoshi
public var shieldedWithPendingBalance: Zatoshi = .zero
public var spendability: Spendability = .everything
public var totalBalance: Zatoshi = .zero
@Shared(.inMemory(.transactions)) public var transactions: IdentifiedArrayOf<TransactionState> = []
public var transparentBalance: Zatoshi
public var feeStr: String {
Zatoshi(100_000).decimalString()
}
public var isPendingTransaction: Bool {
transactions.isAnythingPending()
}
public var isPendingInProcess: Bool {
changePending.amount + pendingTransactions.amount > 0
}
public var isShieldableBalanceAvailable: Bool {
transparentBalance.amount >= autoShieldingThreshold.amount
}
public var isShieldingButtonDisabled: Bool {
isShielding || !isShieldableBalanceAvailable
}
public var isProcessingZeroAvailableBalance: Bool {
if shieldedBalance.amount == 0 && transparentBalance.amount > autoShieldingThreshold.amount {
return false
}
return totalBalance.amount != shieldedBalance.amount && shieldedBalance.amount == 0
}
public init(
autoShieldingThreshold: Zatoshi,
changePending: Zatoshi,
isShielding: Bool,
pendingTransactions: Zatoshi,
shieldedBalance: Zatoshi = .zero,
transparentBalance: Zatoshi = .zero
) {
self.autoShieldingThreshold = autoShieldingThreshold
self.changePending = changePending
self.isShielding = isShielding
self.pendingTransactions = pendingTransactions
self.shieldedBalance = shieldedBalance
self.transparentBalance = transparentBalance
}
}
@CasePathable
public enum Action: Equatable {
case dismissTapped
case everythingSpendable
case onAppear
case onDisappear
case sheetHeightUpdated(CGFloat)
case shieldFundsTapped
case shieldingProcessorStateChanged(ShieldingProcessorClient.State)
case synchronizerStateChanged(RedactableSynchronizerState)
case updateBalance(AccountBalance?)
case updateBalances([AccountUUID: AccountBalance])
case updateBalancesOnAppear
}
@Dependency(\.derivationTool) var derivationTool
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.numberFormatter) var numberFormatter
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.shieldingProcessor) var shieldingProcessor
@Dependency(\.walletStorage) var walletStorage
@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
return .merge(
.publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map { $0.redacted }
.map(Action.synchronizerStateChanged)
}
.cancellable(id: state.stateStreamCancelId, cancelInFlight: true),
.publisher {
shieldingProcessor.observe()
.map(Action.shieldingProcessorStateChanged)
}
.cancellable(id: state.shieldingProcessorCancelId, cancelInFlight: true),
.send(.updateBalancesOnAppear)
)
case .onDisappear:
return .merge(
.cancel(id: state.stateStreamCancelId),
.cancel(id: state.shieldingProcessorCancelId)
)
case .shieldingProcessorStateChanged(let shieldingProcessorState):
state.isShielding = shieldingProcessorState == .requested
if shieldingProcessorState == .succeeded {
return .send(.updateBalancesOnAppear)
}
return .none
case .updateBalancesOnAppear:
guard let account = state.selectedWalletAccount else {
return .none
}
return .run { send in
if let accountBalance = try? await sdkSynchronizer.getAccountsBalances()[account.id] {
await send(.updateBalance(accountBalance))
} else if let accountBalance = sdkSynchronizer.latestState().accountsBalances[account.id] {
await send(.updateBalance(accountBalance))
}
}
case .sheetHeightUpdated:
return .none
case .dismissTapped:
return .none
case .shieldFundsTapped:
shieldingProcessor.shieldFunds()
return .none
case .synchronizerStateChanged(let latestState):
return .send(.updateBalances(latestState.data.accountsBalances))
case .updateBalances(let accountsBalances):
guard let account = state.selectedWalletAccount else {
return .none
}
return .send(.updateBalance(accountsBalances[account.id]))
case .updateBalance(let accountBalance):
state.changePending = (accountBalance?.saplingBalance.changePendingConfirmation ?? .zero) +
(accountBalance?.orchardBalance.changePendingConfirmation ?? .zero)
state.pendingTransactions = (accountBalance?.saplingBalance.valuePendingSpendability ?? .zero) +
(accountBalance?.orchardBalance.valuePendingSpendability ?? .zero)
state.shieldedBalance = (accountBalance?.saplingBalance.spendableValue ?? .zero) + (accountBalance?.orchardBalance.spendableValue ?? .zero)
state.transparentBalance = accountBalance?.unshielded ?? .zero
state.totalBalance = state.shieldedWithPendingBalance + state.transparentBalance
state.shieldedWithPendingBalance = (accountBalance?.saplingBalance.total() ?? .zero) + (accountBalance?.orchardBalance.total() ?? .zero)
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
return .send(.everythingSpendable)
} else {
state.spendability = .something
}
return .none
case .everythingSpendable:
return .none
}
}
}
}
extension IdentifiedArrayOf<TransactionState> {
func isAnythingPending() -> Bool {
return contains(where: \.isPending)
}
}