312 lines
12 KiB
Swift
312 lines
12 KiB
Swift
//
|
|
// BalanceBreakdownStore.swift
|
|
// secant-testnet
|
|
//
|
|
// Created by Lukáš Korba on 04.08.2022.
|
|
//
|
|
|
|
import SwiftUI
|
|
import ComposableArchitecture
|
|
import ZcashLightClientKit
|
|
import DerivationTool
|
|
import MnemonicClient
|
|
import NumberFormatter
|
|
import PartialProposalError
|
|
import Utils
|
|
import Generated
|
|
import WalletStorage
|
|
import SDKSynchronizer
|
|
import Models
|
|
import SyncProgress
|
|
import RestoreWalletStorage
|
|
import ZcashSDKEnvironment
|
|
|
|
public typealias BalanceBreakdownStore = Store<BalanceBreakdownReducer.State, BalanceBreakdownReducer.Action>
|
|
public typealias BalanceBreakdownViewStore = ViewStore<BalanceBreakdownReducer.State, BalanceBreakdownReducer.Action>
|
|
|
|
public struct BalanceBreakdownReducer: Reducer {
|
|
private let CancelId = UUID()
|
|
|
|
public struct State: Equatable {
|
|
public enum Destination: Equatable {
|
|
case partialProposalError
|
|
}
|
|
|
|
@PresentationState public var alert: AlertState<Action>?
|
|
public var autoShieldingThreshold: Zatoshi
|
|
public var changePending: Zatoshi
|
|
public var destination: Destination?
|
|
public var isRestoringWallet = false
|
|
public var isShieldingFunds: Bool
|
|
public var isHintBoxVisible = false
|
|
public var partialProposalErrorState: PartialProposalError.State
|
|
public var pendingTransactions: Zatoshi
|
|
public var shieldedBalance: Zatoshi
|
|
public var totalBalance: Zatoshi
|
|
public var syncProgressState: SyncProgressReducer.State
|
|
public var transparentBalance: Zatoshi
|
|
|
|
public var isShieldableBalanceAvailable: Bool {
|
|
transparentBalance.amount >= autoShieldingThreshold.amount
|
|
}
|
|
|
|
public var isShieldingButtonDisabled: Bool {
|
|
isShieldingFunds || !isShieldableBalanceAvailable
|
|
}
|
|
|
|
public var isProcessingZeroAvailableBalance: Bool {
|
|
totalBalance.amount != shieldedBalance.amount && shieldedBalance.amount == 0
|
|
}
|
|
|
|
public init(
|
|
autoShieldingThreshold: Zatoshi,
|
|
changePending: Zatoshi,
|
|
destination: Destination? = nil,
|
|
isRestoringWallet: Bool = false,
|
|
isShieldingFunds: Bool,
|
|
isHintBoxVisible: Bool = false,
|
|
partialProposalErrorState: PartialProposalError.State,
|
|
pendingTransactions: Zatoshi,
|
|
shieldedBalance: Zatoshi,
|
|
syncProgressState: SyncProgressReducer.State,
|
|
totalBalance: Zatoshi,
|
|
transparentBalance: Zatoshi
|
|
) {
|
|
self.autoShieldingThreshold = autoShieldingThreshold
|
|
self.changePending = changePending
|
|
self.destination = destination
|
|
self.isRestoringWallet = isRestoringWallet
|
|
self.isShieldingFunds = isShieldingFunds
|
|
self.isHintBoxVisible = isHintBoxVisible
|
|
self.partialProposalErrorState = partialProposalErrorState
|
|
self.pendingTransactions = pendingTransactions
|
|
self.shieldedBalance = shieldedBalance
|
|
self.totalBalance = totalBalance
|
|
self.syncProgressState = syncProgressState
|
|
self.transparentBalance = transparentBalance
|
|
}
|
|
}
|
|
|
|
public enum Action: Equatable {
|
|
case alert(PresentationAction<Action>)
|
|
case onAppear
|
|
case onDisappear
|
|
case partialProposalError(PartialProposalError.Action)
|
|
case restoreWalletTask
|
|
case restoreWalletValue(Bool)
|
|
case shieldFunds
|
|
case shieldFundsFailure(ZcashError)
|
|
case shieldFundsPartial([String], [String])
|
|
case shieldFundsSuccess
|
|
case synchronizerStateChanged(RedactableSynchronizerState)
|
|
case syncProgress(SyncProgressReducer.Action)
|
|
case updateDestination(BalanceBreakdownReducer.State.Destination?)
|
|
case updateHintBoxVisibility(Bool)
|
|
}
|
|
|
|
@Dependency(\.derivationTool) var derivationTool
|
|
@Dependency(\.mainQueue) var mainQueue
|
|
@Dependency(\.mnemonic) var mnemonic
|
|
@Dependency(\.numberFormatter) var numberFormatter
|
|
@Dependency(\.restoreWalletStorage) var restoreWalletStorage
|
|
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
|
|
@Dependency(\.walletStorage) var walletStorage
|
|
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
|
|
|
|
public init() { }
|
|
|
|
public var body: some Reducer<State, Action> {
|
|
Scope(state: \.syncProgressState, action: /Action.syncProgress) {
|
|
SyncProgressReducer()
|
|
}
|
|
|
|
Scope(state: \.partialProposalErrorState, action: /Action.partialProposalError) {
|
|
PartialProposalError()
|
|
}
|
|
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .alert(.presented(let action)):
|
|
return Effect.send(action)
|
|
|
|
case .alert(.dismiss):
|
|
state.alert = nil
|
|
return .none
|
|
|
|
case .alert:
|
|
return .none
|
|
|
|
case .onAppear:
|
|
state.autoShieldingThreshold = zcashSDKEnvironment.shieldingThreshold
|
|
return .publisher {
|
|
sdkSynchronizer.stateStream()
|
|
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
|
.map { $0.redacted }
|
|
.map(Action.synchronizerStateChanged)
|
|
}
|
|
.cancellable(id: CancelId, cancelInFlight: true)
|
|
|
|
case .onDisappear:
|
|
return .cancel(id: CancelId)
|
|
|
|
case .partialProposalError:
|
|
return .none
|
|
|
|
case .restoreWalletTask:
|
|
return .run { send in
|
|
for await value in await restoreWalletStorage.value() {
|
|
await send(.restoreWalletValue(value))
|
|
}
|
|
}
|
|
|
|
case .restoreWalletValue(let value):
|
|
state.isRestoringWallet = value
|
|
return .none
|
|
|
|
case .shieldFunds:
|
|
state.isShieldingFunds = true
|
|
return .run { send in
|
|
do {
|
|
let storedWallet = try walletStorage.exportWallet()
|
|
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
|
|
let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0, zcashSDKEnvironment.network.networkType)
|
|
|
|
guard let uAddress = try await sdkSynchronizer.getUnifiedAddress(0) else { throw "sdkSynchronizer.getUnifiedAddress" }
|
|
|
|
let address = try uAddress.transparentReceiver()
|
|
let proposal = try await sdkSynchronizer.proposeShielding(0, zcashSDKEnvironment.shieldingThreshold, .empty, address)
|
|
|
|
guard let proposal else { throw "sdkSynchronizer.proposeShielding" }
|
|
|
|
let result = try await sdkSynchronizer.createProposedTransactions(proposal, spendingKey)
|
|
|
|
switch result {
|
|
case .failure:
|
|
await send(.shieldFundsFailure("sdkSynchronizer.createProposedTransactions".toZcashError()))
|
|
case let .partial(txIds: txIds, statuses: statuses):
|
|
await send(.shieldFundsPartial(txIds, statuses))
|
|
case .success:
|
|
await send(.shieldFundsSuccess)
|
|
}
|
|
} catch {
|
|
await send(.shieldFundsFailure(error.toZcashError()))
|
|
}
|
|
}
|
|
|
|
case .shieldFundsFailure(let error):
|
|
state.isShieldingFunds = false
|
|
state.alert = AlertState.shieldFundsFailure(error)
|
|
return .none
|
|
|
|
case .shieldFundsSuccess:
|
|
state.isShieldingFunds = false
|
|
state.transparentBalance = .zero
|
|
return .none
|
|
|
|
case let .shieldFundsPartial(txIds, statuses):
|
|
state.partialProposalErrorState.txIds = txIds
|
|
state.partialProposalErrorState.statuses = statuses
|
|
return .send(.updateDestination(.partialProposalError))
|
|
|
|
case .synchronizerStateChanged(let latestState):
|
|
let accountBalance = latestState.data.accountBalance?.data
|
|
|
|
state.shieldedBalance = (accountBalance?.saplingBalance.spendableValue ?? .zero) + (accountBalance?.orchardBalance.spendableValue ?? .zero)
|
|
state.totalBalance = (accountBalance?.saplingBalance.total() ?? .zero) + (accountBalance?.orchardBalance.total() ?? .zero)
|
|
state.transparentBalance = accountBalance?.unshielded ?? .zero
|
|
state.changePending = (accountBalance?.saplingBalance.changePendingConfirmation ?? .zero) + (accountBalance?.orchardBalance.changePendingConfirmation ?? .zero)
|
|
state.pendingTransactions = (accountBalance?.saplingBalance.valuePendingSpendability ?? .zero) + (accountBalance?.orchardBalance.valuePendingSpendability ?? .zero)
|
|
return .none
|
|
|
|
case .syncProgress:
|
|
return .none
|
|
|
|
case let .updateDestination(destination):
|
|
state.destination = destination
|
|
return .none
|
|
|
|
case .updateHintBoxVisibility(let visibility):
|
|
state.isHintBoxVisible = visibility
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Alerts
|
|
|
|
extension AlertState where Action == BalanceBreakdownReducer.Action {
|
|
public static func shieldFundsFailure(_ error: ZcashError) -> AlertState {
|
|
AlertState {
|
|
TextState(L10n.Balances.Alert.ShieldFunds.Failure.title)
|
|
} message: {
|
|
TextState(L10n.Balances.Alert.ShieldFunds.Failure.message(error.detailedMessage))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Store
|
|
|
|
extension BalanceBreakdownStore {
|
|
func partialProposalErrorStore() -> StoreOf<PartialProposalError> {
|
|
self.scope(
|
|
state: \.partialProposalErrorState,
|
|
action: BalanceBreakdownReducer.Action.partialProposalError
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewStore
|
|
|
|
extension BalanceBreakdownViewStore {
|
|
var destinationBinding: Binding<BalanceBreakdownReducer.State.Destination?> {
|
|
self.binding(
|
|
get: \.destination,
|
|
send: BalanceBreakdownReducer.Action.updateDestination
|
|
)
|
|
}
|
|
|
|
var bindingForPartialProposalError: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: { $0 == .partialProposalError },
|
|
embed: { $0 ? BalanceBreakdownReducer.State.Destination.partialProposalError : nil }
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Placeholders
|
|
|
|
extension BalanceBreakdownReducer.State {
|
|
public static let placeholder = BalanceBreakdownReducer.State(
|
|
autoShieldingThreshold: .zero,
|
|
changePending: .zero,
|
|
isShieldingFunds: false,
|
|
partialProposalErrorState: .initial,
|
|
pendingTransactions: .zero,
|
|
shieldedBalance: .zero,
|
|
syncProgressState: .initial,
|
|
totalBalance: .zero,
|
|
transparentBalance: .zero
|
|
)
|
|
|
|
public static let initial = BalanceBreakdownReducer.State(
|
|
autoShieldingThreshold: .zero,
|
|
changePending: .zero,
|
|
isShieldingFunds: false,
|
|
partialProposalErrorState: .initial,
|
|
pendingTransactions: .zero,
|
|
shieldedBalance: .zero,
|
|
syncProgressState: .initial,
|
|
totalBalance: .zero,
|
|
transparentBalance: .zero
|
|
)
|
|
}
|
|
|
|
extension BalanceBreakdownStore {
|
|
public static let placeholder = BalanceBreakdownStore(
|
|
initialState: .placeholder
|
|
) {
|
|
BalanceBreakdownReducer()
|
|
}
|
|
}
|