321 lines
11 KiB
Swift
321 lines
11 KiB
Swift
//
|
|
// SendFlowStore.swift
|
|
// secant-testnet
|
|
//
|
|
// Created by Lukáš Korba on 04/25/2022.
|
|
//
|
|
|
|
import SwiftUI
|
|
import ComposableArchitecture
|
|
import ZcashLightClientKit
|
|
|
|
typealias SendFlowStore = Store<SendFlowReducer.State, SendFlowReducer.Action>
|
|
typealias SendFlowViewStore = ViewStore<SendFlowReducer.State, SendFlowReducer.Action>
|
|
|
|
struct SendFlowReducer: ReducerProtocol {
|
|
private enum SyncStatusUpdatesID {}
|
|
|
|
struct State: Equatable {
|
|
enum Destination: Equatable {
|
|
case confirmation
|
|
case inProgress
|
|
case success
|
|
case failure
|
|
case done
|
|
}
|
|
|
|
var addMemoState: Bool
|
|
var isSendingTransaction = false
|
|
var memoState: MultiLineTextFieldReducer.State
|
|
var destination: Destination?
|
|
var shieldedBalance = WalletBalance.zero
|
|
var transactionAddressInputState: TransactionAddressTextFieldReducer.State
|
|
var transactionAmountInputState: TransactionAmountTextFieldReducer.State
|
|
|
|
var address: String {
|
|
get { transactionAddressInputState.textFieldState.text }
|
|
set { transactionAddressInputState.textFieldState.text = newValue }
|
|
}
|
|
|
|
var amount: Zatoshi {
|
|
get { Zatoshi(transactionAmountInputState.amount) }
|
|
set {
|
|
transactionAmountInputState.amount = newValue.amount
|
|
transactionAmountInputState.textFieldState.text = newValue.amount == 0 ?
|
|
"" :
|
|
newValue.decimalString()
|
|
}
|
|
}
|
|
|
|
var isInvalidAddressFormat: Bool {
|
|
!transactionAddressInputState.isValidAddress
|
|
&& !transactionAddressInputState.textFieldState.text.isEmpty
|
|
}
|
|
|
|
var isInvalidAmountFormat: Bool {
|
|
!transactionAmountInputState.textFieldState.valid
|
|
&& !transactionAmountInputState.textFieldState.text.isEmpty
|
|
}
|
|
|
|
var isValidForm: Bool {
|
|
transactionAmountInputState.amount > 0
|
|
&& transactionAddressInputState.isValidAddress
|
|
&& !isInsufficientFunds
|
|
&& memoState.isValid
|
|
}
|
|
|
|
var isInsufficientFunds: Bool {
|
|
transactionAmountInputState.amount > transactionAmountInputState.maxValue
|
|
}
|
|
|
|
var totalCurrencyBalance: Zatoshi {
|
|
Zatoshi.from(decimal: shieldedBalance.total.decimalValue.decimalValue * transactionAmountInputState.zecPrice)
|
|
}
|
|
}
|
|
|
|
enum Action: Equatable {
|
|
case addMemo(CheckCircleReducer.Action)
|
|
case memo(MultiLineTextFieldReducer.Action)
|
|
case onAppear
|
|
case onDisappear
|
|
case sendConfirmationPressed
|
|
case sendTransactionResult(Result<TransactionState, NSError>)
|
|
case synchronizerStateChanged(SDKSynchronizerState)
|
|
case transactionAddressInput(TransactionAddressTextFieldReducer.Action)
|
|
case transactionAmountInput(TransactionAmountTextFieldReducer.Action)
|
|
case updateDestination(SendFlowReducer.State.Destination?)
|
|
}
|
|
|
|
@Dependency(\.derivationTool) var derivationTool
|
|
@Dependency(\.mainQueue) var mainQueue
|
|
@Dependency(\.mnemonic) var mnemonic
|
|
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
|
|
@Dependency(\.walletStorage) var walletStorage
|
|
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
|
|
|
|
var body: some ReducerProtocol<State, Action> {
|
|
Scope(state: \.addMemoState, action: /Action.addMemo) {
|
|
CheckCircleReducer()
|
|
}
|
|
|
|
Scope(state: \.memoState, action: /Action.memo) {
|
|
MultiLineTextFieldReducer()
|
|
}
|
|
|
|
Scope(state: \.transactionAddressInputState, action: /Action.transactionAddressInput) {
|
|
TransactionAddressTextFieldReducer()
|
|
}
|
|
|
|
Scope(state: \.transactionAmountInputState, action: /Action.transactionAmountInput) {
|
|
TransactionAmountTextFieldReducer()
|
|
}
|
|
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .addMemo:
|
|
return .none
|
|
|
|
case .updateDestination(.done):
|
|
state.destination = nil
|
|
state.memoState.text = ""
|
|
state.transactionAmountInputState.textFieldState.text = ""
|
|
state.transactionAmountInputState.amount = 0
|
|
state.transactionAddressInputState.textFieldState.text = ""
|
|
return .none
|
|
|
|
case .updateDestination(.failure):
|
|
state.destination = .failure
|
|
state.isSendingTransaction = false
|
|
return .none
|
|
|
|
case .updateDestination(.confirmation):
|
|
state.amount = Zatoshi(state.transactionAmountInputState.amount)
|
|
state.address = state.transactionAddressInputState.textFieldState.text
|
|
state.destination = .confirmation
|
|
return .none
|
|
|
|
case let .updateDestination(destination):
|
|
state.destination = destination
|
|
return .none
|
|
|
|
case .sendConfirmationPressed:
|
|
guard !state.isSendingTransaction else {
|
|
return .none
|
|
}
|
|
|
|
do {
|
|
let storedWallet = try walletStorage.exportWallet()
|
|
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase)
|
|
let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0)
|
|
|
|
state.isSendingTransaction = true
|
|
|
|
let memo: Memo?
|
|
if let memoText = state.addMemoState ? state.memoState.text : nil {
|
|
memo = try Memo(string: memoText)
|
|
} else {
|
|
memo = nil
|
|
}
|
|
|
|
let recipient = try Recipient(state.address, network: zcashSDKEnvironment.network.networkType)
|
|
let sendTransActionEffect = sdkSynchronizer.sendTransaction(
|
|
with: spendingKey,
|
|
zatoshi: state.amount,
|
|
to: recipient,
|
|
memo: memo
|
|
)
|
|
.receive(on: mainQueue)
|
|
.map(SendFlowReducer.Action.sendTransactionResult)
|
|
.eraseToEffect()
|
|
|
|
return .concatenate(
|
|
Effect(value: .updateDestination(.inProgress)),
|
|
sendTransActionEffect
|
|
)
|
|
} catch {
|
|
return Effect(value: .updateDestination(.failure))
|
|
}
|
|
|
|
case .sendTransactionResult(let result):
|
|
state.isSendingTransaction = false
|
|
do {
|
|
_ = try result.get()
|
|
return Effect(value: .updateDestination(.success))
|
|
} catch {
|
|
return Effect(value: .updateDestination(.failure))
|
|
}
|
|
|
|
case .transactionAmountInput:
|
|
return .none
|
|
|
|
case .transactionAddressInput:
|
|
return .none
|
|
|
|
case .onAppear:
|
|
state.memoState.charLimit = zcashSDKEnvironment.memoCharLimit
|
|
return sdkSynchronizer.stateChanged
|
|
.map(SendFlowReducer.Action.synchronizerStateChanged)
|
|
.eraseToEffect()
|
|
.cancellable(id: SyncStatusUpdatesID.self, cancelInFlight: true)
|
|
|
|
case .onDisappear:
|
|
return Effect.cancel(id: SyncStatusUpdatesID.self)
|
|
|
|
case .synchronizerStateChanged(.synced):
|
|
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState?.shieldedBalance {
|
|
state.shieldedBalance = shieldedBalance
|
|
state.transactionAmountInputState.maxValue = shieldedBalance.total.amount
|
|
}
|
|
return .none
|
|
|
|
case .synchronizerStateChanged:
|
|
return .none
|
|
|
|
case .memo:
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Store
|
|
|
|
extension SendFlowStore {
|
|
func addMemoStore() -> CheckCircleStore {
|
|
self.scope(
|
|
state: \.addMemoState,
|
|
action: SendFlowReducer.Action.addMemo
|
|
)
|
|
}
|
|
|
|
func memoStore() -> MultiLineTextFieldStore {
|
|
self.scope(
|
|
state: \.memoState,
|
|
action: SendFlowReducer.Action.memo
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewStore
|
|
|
|
extension SendFlowViewStore {
|
|
var destinationBinding: Binding<SendFlowReducer.State.Destination?> {
|
|
self.binding(
|
|
get: \.destination,
|
|
send: SendFlowReducer.Action.updateDestination
|
|
)
|
|
}
|
|
|
|
var bindingForConfirmation: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: {
|
|
$0 == .confirmation ||
|
|
$0 == .inProgress ||
|
|
$0 == .success ||
|
|
$0 == .failure
|
|
},
|
|
embed: { $0 ? SendFlowReducer.State.Destination.confirmation : nil }
|
|
)
|
|
}
|
|
|
|
var bindingForInProgress: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: {
|
|
$0 == .inProgress ||
|
|
$0 == .success ||
|
|
$0 == .failure
|
|
},
|
|
embed: { $0 ? SendFlowReducer.State.Destination.inProgress : SendFlowReducer.State.Destination.confirmation }
|
|
)
|
|
}
|
|
|
|
var bindingForSuccess: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: { $0 == .success },
|
|
embed: { _ in SendFlowReducer.State.Destination.success }
|
|
)
|
|
}
|
|
|
|
var bindingForFailure: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: { $0 == .failure },
|
|
embed: { _ in SendFlowReducer.State.Destination.failure }
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Placeholders
|
|
|
|
extension SendFlowReducer.State {
|
|
static var placeholder: Self {
|
|
.init(
|
|
addMemoState: true,
|
|
memoState: .placeholder,
|
|
destination: nil,
|
|
transactionAddressInputState: .placeholder,
|
|
transactionAmountInputState: .amount
|
|
)
|
|
}
|
|
|
|
static var emptyPlaceholder: Self {
|
|
.init(
|
|
addMemoState: true,
|
|
memoState: .placeholder,
|
|
destination: nil,
|
|
transactionAddressInputState: .placeholder,
|
|
transactionAmountInputState: .placeholder
|
|
)
|
|
}
|
|
}
|
|
|
|
// #if DEBUG // FIX: Issue #306 - Release build is broken
|
|
extension SendFlowStore {
|
|
static var placeholder: SendFlowStore {
|
|
return SendFlowStore(
|
|
initialState: .emptyPlaceholder,
|
|
reducer: SendFlowReducer()
|
|
)
|
|
}
|
|
}
|
|
// #endif
|