[#981] Send button disable check must include the fee

- the fee is now checked and it's coming from the SDK
- unit test checking validity of the form when not enough Zatoshi for the fee has been implemented
- brand new concept of handling the SDK constants inside the TCA has been implemented, will greatly simplify cleanup of all reducers (see #1058)
This commit is contained in:
Lukas Korba 2024-02-14 12:44:42 +01:00
parent b668b15f04
commit 0faa37c243
9 changed files with 109 additions and 51 deletions

View File

@ -13,18 +13,22 @@ import DatabaseFiles
import Models
import ZcashSDKEnvironment
extension SDKSynchronizerClient {
extension SDKSynchronizerClient: DependencyKey {
public static let liveValue: SDKSynchronizerClient = Self.live()
public static func live(
databaseFiles: DatabaseFilesClient = .liveValue,
environment: ZcashSDKEnvironment = .liveValue,
network: ZcashNetwork
databaseFiles: DatabaseFilesClient = .liveValue
) -> Self {
@Dependency (\.zcashSDKEnvironment) var zcashSDKEnvironment
let network = zcashSDKEnvironment.network
let initializer = Initializer(
cacheDbURL: databaseFiles.cacheDbURLFor(network),
fsBlockDbRoot: databaseFiles.fsBlockDbRootFor(network),
generalStorageURL: databaseFiles.documentsDirectory(),
dataDbURL: databaseFiles.dataDbURLFor(network),
endpoint: environment.endpoint(network),
endpoint: zcashSDKEnvironment.endpoint(),
network: network,
spendParamsURL: databaseFiles.spendParamsURLFor(network),
outputParamsURL: databaseFiles.outputParamsURLFor(network),

View File

@ -108,7 +108,7 @@ extension ZcashSDKEnvironment {
static let streamingCallTimeoutInMillis = Int64(10 * 60 * 60 * 1000) // ten hours
}
public static func endpoint(for network: ZcashNetwork) -> String {
public static func endpointString(for network: ZcashNetwork) -> String {
switch network.networkType {
case .testnet:
return ZcashSDKConstants.endpointTestnetAddress
@ -119,10 +119,12 @@ extension ZcashSDKEnvironment {
}
public struct ZcashSDKEnvironment {
public var latestCheckpoint: (ZcashNetwork) -> BlockHeight //{ BlockHeight.ofLatestCheckpoint(network: network()) }
public let endpoint: (ZcashNetwork) -> LightWalletEndpoint
public var latestCheckpoint: BlockHeight
public let endpoint: () -> LightWalletEndpoint
public let memoCharLimit: Int
public let mnemonicWordsMaxCount: Int
public let network: ZcashNetwork
public let requiredTransactionConfirmations: Int
public let sdkVersion: String
public let tokenName: String
}

View File

@ -8,40 +8,44 @@
import ComposableArchitecture
import ZcashLightClientKit
extension ZcashSDKEnvironment: DependencyKey {
public static let liveValue = Self(
latestCheckpoint: { network in BlockHeight.ofLatestCheckpoint(network: network) },
endpoint: { network in
// In case of mainnet network we may have stored server as a user action in advanced settings
if network.networkType == .mainnet {
@Dependency(\.userDefaults) var userDefaults
let udKey = ZcashSDKEnvironment.Servers.Constants.udServerKey
if let storedServerRaw = userDefaults.objectForKey(udKey) as? String,
let storedServer = ZcashSDKEnvironment.Servers(rawValue: storedServerRaw) {
if let endpoint = storedServer.lightWalletEndpoint(userDefaults) {
// Some endpoint is set by a user so we initialize the SDK with this one
return endpoint
} else {
// Initalization of LightWalletEndpoint failed, fallback to hardcoded one,
// setting the mainnet key to the storage to reflect that
userDefaults.setValue(ZcashSDKEnvironment.Servers.mainnet.rawValue, udKey)
extension ZcashSDKEnvironment {
public static func live(network: ZcashNetwork) -> Self {
Self(
latestCheckpoint: BlockHeight.ofLatestCheckpoint(network: network),
endpoint: {
// In case of mainnet network we may have stored server as a user action in advanced settings
if network.networkType == .mainnet {
@Dependency(\.userDefaults) var userDefaults
let udKey = ZcashSDKEnvironment.Servers.Constants.udServerKey
if let storedServerRaw = userDefaults.objectForKey(udKey) as? String,
let storedServer = ZcashSDKEnvironment.Servers(rawValue: storedServerRaw) {
if let endpoint = storedServer.lightWalletEndpoint(userDefaults) {
// Some endpoint is set by a user so we initialize the SDK with this one
return endpoint
} else {
// Initalization of LightWalletEndpoint failed, fallback to hardcoded one,
// setting the mainnet key to the storage to reflect that
userDefaults.setValue(ZcashSDKEnvironment.Servers.mainnet.rawValue, udKey)
}
}
}
}
// Hardcoded endpoint
return LightWalletEndpoint(
address: Self.endpoint(for: network),
port: ZcashSDKConstants.endpointPort,
secure: true,
streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis
)
},
memoCharLimit: MemoBytes.capacity,
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
sdkVersion: "0.18.1-beta"
)
// Hardcoded endpoint
return LightWalletEndpoint(
address: Self.endpointString(for: network),
port: ZcashSDKConstants.endpointPort,
secure: true,
streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis
)
},
memoCharLimit: MemoBytes.capacity,
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
network: network,
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
sdkVersion: "0.18.1-beta",
tokenName: network.networkType == .testnet ? "TAZ" : "ZEC"
)
}
}

View File

@ -10,11 +10,11 @@ import ZcashLightClientKit
import XCTestDynamicOverlay
extension ZcashSDKEnvironment: TestDependencyKey {
public static let testnet = ZcashSDKEnvironment.liveValue
public static let testnet = ZcashSDKEnvironment.live(network: ZcashNetworkBuilder.network(for: .testnet))
public static let testValue = Self(
latestCheckpoint: { _ in 0 },
endpoint: { _ in
latestCheckpoint: 0,
endpoint: {
LightWalletEndpoint(
address: ZcashSDKConstants.endpointTestnetAddress,
port: ZcashSDKConstants.endpointPort,
@ -24,7 +24,9 @@ extension ZcashSDKEnvironment: TestDependencyKey {
},
memoCharLimit: MemoBytes.capacity,
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
network: ZcashNetworkBuilder.network(for: .testnet),
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
sdkVersion: "0.18.1-beta"
sdkVersion: "0.18.1-beta",
tokenName: "TAZ"
)
}

View File

@ -240,7 +240,7 @@ extension RootReducer {
return .none
}
let birthday = state.storedWallet?.birthday?.value() ?? zcashSDKEnvironment.latestCheckpoint(zcashNetwork)
let birthday = state.storedWallet?.birthday?.value() ?? zcashSDKEnvironment.latestCheckpoint
try mnemonic.isValid(storedWallet.seedPhrase.value())
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())

View File

@ -93,7 +93,7 @@ public struct SecurityWarningReducer: Reducer {
do {
// get the random english mnemonic
let newRandomPhrase = try mnemonic.randomMnemonic()
let birthday = zcashSDKEnvironment.latestCheckpoint(zcashNetwork)
let birthday = zcashSDKEnvironment.latestCheckpoint
// store the wallet to the keychain
try walletStorage.importWallet(newRandomPhrase, birthday, .english, false)

View File

@ -78,7 +78,9 @@ public struct SendFlowReducer: Reducer {
}
public var isValidForm: Bool {
transactionAmountInputState.amount.data > 0
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
return transactionAmountInputState.amount.data > zcashSDKEnvironment.network.constants.defaultFee().amount
&& transactionAddressInputState.isValidAddress
&& !isInsufficientFunds
&& memoState.isValid

View File

@ -12,6 +12,7 @@ import ZcashLightClientKit
import SDKSynchronizer
import Utils
import Root
import ZcashSDKEnvironment
@main
struct SecantApp: App {
@ -74,6 +75,6 @@ public enum TargetConstants {
}
}
extension SDKSynchronizerClient: DependencyKey {
public static let liveValue: SDKSynchronizerClient = Self.live(network: TargetConstants.zcashNetwork)
extension ZcashSDKEnvironment: DependencyKey {
public static let liveValue: ZcashSDKEnvironment = Self.live(network: TargetConstants.zcashNetwork)
}

View File

@ -409,6 +409,49 @@ class SendTests: XCTestCase {
}
}
func testValidForm_NoFees() async throws {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .initial,
scanState: .initial,
transactionAddressInputState: .initial,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
amount: Int64(9_000).redacted,
currencySelectionState: CurrencySelectionReducer.State(),
maxValue: Int64(501_302).redacted,
textFieldState:
TCATextFieldReducer.State(
validationType: .customFloatingPoint(usNumberFormatter),
text: "0.00501301".redacted
)
)
)
let store = TestStore(
initialState: sendState
) {
SendFlowReducer(networkType: .testnet)
}
store.dependencies.derivationTool = .noOp
store.dependencies.derivationTool.isZcashAddress = { _, _ in true }
let address = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po".redacted
await store.send(.transactionAddressInput(.textField(.set(address)))) { state in
state.transactionAddressInputState.textFieldState.text = address
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
func testInvalidForm_InsufficientFunds() async throws {
let sendState = SendFlowReducer.State(
addMemoState: true,