From b2ae82ce1b6ed20ace8d11931bfd8e29d2af5a67 Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Tue, 31 May 2022 19:14:56 +0200 Subject: [PATCH] [#329] Update wallet to use Zatoshi type (#333) - Int64+Zcash.swift and Double+Zcash.swift removed - Balances and amounts updated to use Zatoshi - Remove TODOs - Tests updated - FIXED: Send amount being in Zatoshi is clamping $ value to 21M max -> send amount input is no longer Zatoshi typed [329] Update wallet to use Zatoshi type (333) - alphabetical order(s) [329] Update wallet to use Zatoshi type (333) - static .zero for the Zatoshi - conformance to Equatable moved to extension [329] Update wallet to use Zatoshi type (333) - small improvement by reducing code duplicity --- secant.xcodeproj/project.pbxproj | 8 - secant/Features/Home/HomeStore.swift | 20 +- secant/Features/Home/HomeView.swift | 2 +- secant/Features/Sandbox/SandboxView.swift | 8 +- secant/Features/SendFlow/SendFlowStore.swift | 33 ++-- secant/Features/SendFlow/SendFlowView.swift | 8 +- .../Views/CreateTransactionView.swift | 8 +- .../Views/TransactionConfirmationView.swift | 2 +- .../TransactionHistoryFlowStore.swift | 4 +- .../TransactionHistoryFlowView.swift | 2 +- secant/Models/SendFlowTransaction.swift | 4 +- secant/Models/TransactionState.swift | 12 +- .../TransactionAddressTextFieldStore.swift | 2 +- .../TransactionAmountTextField.swift | 4 +- .../TransactionAmountTextFieldStore.swift | 102 ++++++----- secant/Utils/Double+Zcash.swift | 19 -- secant/Utils/Int64+Zcash.swift | 19 -- secant/Utils/Zatoshi.swift | 22 ++- secant/Wrappers/WrappedSDKSynchronizer.swift | 54 +++--- secantTests/SendTests/SendTests.swift | 173 ++++++++++-------- .../TransactionAmountInputTests.swift | 148 +++++++++------ .../TransactionHistoryTests.swift | 24 ++- secantTests/UtilTests/ZatoshiTests.swift | 8 +- 23 files changed, 358 insertions(+), 328 deletions(-) delete mode 100644 secant/Utils/Double+Zcash.swift delete mode 100644 secant/Utils/Int64+Zcash.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index c13214b..c3f43e0 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -95,14 +95,12 @@ 9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */; }; 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */; }; 9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */; }; - 9E2F1C8228095AFE004E65FE /* Int64+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */; }; 9E2F1C842809B606004E65FE /* DebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2F1C832809B606004E65FE /* DebugMenu.swift */; }; 9E2F1C8C280ED6A7004E65FE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9E2F1C8B280ED6A7004E65FE /* LaunchScreen.storyboard */; }; 9E2F1C8F280EDE09004E65FE /* Drawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2F1C8E280EDE09004E65FE /* Drawer.swift */; }; 9E37A2B827C8F59F00AE57B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */; }; 9E391124283E4CAC0073DD9A /* ImportWalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E391123283E4CAC0073DD9A /* ImportWalletTests.swift */; }; 9E391129283F74590073DD9A /* Zatoshi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E391128283F74590073DD9A /* Zatoshi.swift */; }; - 9E39112A283F90F10073DD9A /* Double+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */; }; 9E39112E283F91600073DD9A /* ZatoshiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E39112D283F91600073DD9A /* ZatoshiTests.swift */; }; 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */; }; 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; }; @@ -287,7 +285,6 @@ 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletStore.swift; sourceTree = ""; }; 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportSeedEditor.swift; sourceTree = ""; }; 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletView.swift; sourceTree = ""; }; - 9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int64+Zcash.swift"; sourceTree = ""; }; 9E2F1C832809B606004E65FE /* DebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugMenu.swift; sourceTree = ""; }; 9E2F1C8B280ED6A7004E65FE /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 9E2F1C8E280EDE09004E65FE /* Drawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawer.swift; sourceTree = ""; }; @@ -330,7 +327,6 @@ 9EAFEB8E2808183D00199FC9 /* SandboxStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxStore.swift; sourceTree = ""; }; 9EBEF87927CE369800B4F343 /* RecoveryPhraseValidationFlowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidationFlowView.swift; sourceTree = ""; }; 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFiles.swift; sourceTree = ""; }; - 9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Zcash.swift"; sourceTree = ""; }; 9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencySelectionTests.swift; sourceTree = ""; }; 9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputTests.swift; sourceTree = ""; }; 9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputTests.swift; sourceTree = ""; }; @@ -824,12 +820,10 @@ children = ( F96B41EA273B50520021B49A /* Strings.swift */, 9E7FE0D4282D281800C374E8 /* Array+Chunked.swift */, - 9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */, 0DACFA8027208D940039EEA5 /* UInt+SuperscriptText.swift */, 0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */, 9E7FE0D2282D274E00C374E8 /* Date+Readable.swift */, 9E7FE0CE282D257400C374E8 /* SDKSynchronizer+SyncStatus.swift */, - 9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */, 0DACFA7E27208CE00039EEA5 /* Clamped.swift */, F9322DBF273B555C00C105B5 /* NavigationLinks.swift */, F9C165B3274031F600592F76 /* Bindings.swift */, @@ -1286,7 +1280,6 @@ 9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */, 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */, F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */, - 9E2F1C8228095AFE004E65FE /* Int64+Zcash.swift in Sources */, 9E02B56A27FED43E005B809B /* WrappedFileManager.swift in Sources */, 663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */, 0DC487C32772574C00BE6A63 /* RecoveryPhraseBackupSucceededView.swift in Sources */, @@ -1310,7 +1303,6 @@ 0DDB6A5127737D4A0012A410 /* RecoveryPhraseBackupFailedView.swift in Sources */, 9E391129283F74590073DD9A /* Zatoshi.swift in Sources */, 0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */, - 9E39112A283F90F10073DD9A /* Double+Zcash.swift in Sources */, 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */, F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */, 669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */, diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index 6e213af..0b4745b 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -27,9 +27,9 @@ struct HomeState: Equatable { var sendState: SendFlowState var scanState: ScanState var synchronizerStatus: String - var totalBalance: Int64 + var totalBalance: Zatoshi var transactionHistoryState: TransactionHistoryFlowState - var verifiedBalance: Int64 + var verifiedBalance: Zatoshi } // MARK: Action @@ -110,8 +110,8 @@ extension HomeReducer { return Effect(value: .updateSynchronizerStatus) case .updateBalance(let balance): - state.totalBalance = balance.total - state.verifiedBalance = balance.verified + state.totalBalance = Zatoshi(amount: balance.total) + state.verifiedBalance = Zatoshi(amount: balance.verified) return .none case .updateDrawer(let drawerOverlay): @@ -179,11 +179,11 @@ extension HomeReducer { action: /HomeAction.send, environment: { environment in SendFlowEnvironment( - mnemonic: environment.mnemonic, - scheduler: environment.scheduler, - walletStorage: environment.walletStorage, derivationTool: environment.derivationTool, - SDKSynchronizer: environment.SDKSynchronizer + mnemonic: environment.mnemonic, + SDKSynchronizer: environment.SDKSynchronizer, + scheduler: environment.scheduler, + walletStorage: environment.walletStorage ) } ) @@ -282,9 +282,9 @@ extension HomeState { sendState: .placeholder, scanState: .placeholder, synchronizerStatus: "", - totalBalance: 0, + totalBalance: Zatoshi.zero, transactionHistoryState: .emptyPlaceHolder, - verifiedBalance: 0 + verifiedBalance: Zatoshi.zero ) } } diff --git a/secant/Features/Home/HomeView.swift b/secant/Features/Home/HomeView.swift index cace642..4754722 100644 --- a/secant/Features/Home/HomeView.swift +++ b/secant/Features/Home/HomeView.swift @@ -18,7 +18,7 @@ struct HomeView: View { Text("\(viewStore.synchronizerStatus)") .padding(.top, 60) - Text("balance \(viewStore.totalBalance.asZecString()) ZEC") + Text("balance \(viewStore.totalBalance.decimalString()) ZEC") .accessDebugMenuWithHiddenGesture { viewStore.send(.debugMenuStartup) } diff --git a/secant/Features/Sandbox/SandboxView.swift b/secant/Features/Sandbox/SandboxView.swift index ce4a729..62e7976 100644 --- a/secant/Features/Sandbox/SandboxView.swift +++ b/secant/Features/Sandbox/SandboxView.swift @@ -32,11 +32,11 @@ struct SandboxView: View { ) .debug(), environment: SendFlowEnvironment( - mnemonic: .live, - scheduler: DispatchQueue.main.eraseToAnyScheduler(), - walletStorage: .live(), derivationTool: .live(), - SDKSynchronizer: LiveWrappedSDKSynchronizer() + mnemonic: .live, + SDKSynchronizer: LiveWrappedSDKSynchronizer(), + scheduler: DispatchQueue.main.eraseToAnyScheduler(), + walletStorage: .live() ) ) ) diff --git a/secant/Features/SendFlow/SendFlowStore.swift b/secant/Features/SendFlow/SendFlowStore.swift index 0aaeca3..ef35079 100644 --- a/secant/Features/SendFlow/SendFlowStore.swift +++ b/secant/Features/SendFlow/SendFlowStore.swift @@ -23,11 +23,10 @@ struct SendFlowState: Equatable { case done } - var route: Route? - var isSendingTransaction = false var memo = "" - var totalBalance: Int64 = 0 + var route: Route? + var totalBalance = Zatoshi.zero var transaction: SendFlowTransaction var transactionAddressInputState: TransactionAddressTextFieldState var transactionAmountInputState: TransactionAmountTextFieldState @@ -52,8 +51,8 @@ struct SendFlowState: Equatable { transactionAmountInputState.amount > transactionAmountInputState.maxValue } - var totalCurrencyBalance: Int64 { - (totalBalance.asHumanReadableZecBalance() * transactionAmountInputState.zecPrice).asZec() + var totalCurrencyBalance: Zatoshi { + Zatoshi.from(decimal: totalBalance.decimalValue.decimalValue * transactionAmountInputState.zecPrice) } } @@ -67,7 +66,7 @@ enum SendFlowAction: Equatable { case synchronizerStateChanged(WrappedSDKSynchronizerState) case transactionAddressInput(TransactionAddressTextFieldAction) case transactionAmountInput(TransactionAmountTextFieldAction) - case updateBalance(Int64) + case updateBalance(Zatoshi) case updateMemo(String) case updateTransaction(SendFlowTransaction) case updateRoute(SendFlowState.Route?) @@ -76,11 +75,11 @@ enum SendFlowAction: Equatable { // MARK: - Environment struct SendFlowEnvironment { + let derivationTool: WrappedDerivationTool let mnemonic: WrappedMnemonic + let SDKSynchronizer: WrappedSDKSynchronizer let scheduler: AnySchedulerOf let walletStorage: WrappedWalletStorage - let derivationTool: WrappedDerivationTool - let SDKSynchronizer: WrappedSDKSynchronizer } // MARK: - Reducer @@ -109,7 +108,7 @@ extension SendFlowReducer { return .none case .updateRoute(.confirmation): - state.transaction.amount = state.transactionAmountInputState.amount + state.transaction.amount = Zatoshi(amount: state.transactionAmountInputState.amount) state.transaction.toAddress = state.transactionAddressInputState.textFieldState.text return .none @@ -133,7 +132,7 @@ extension SendFlowReducer { return environment.SDKSynchronizer.sendTransaction( with: spendingKey, - zatoshi: Int64(state.transaction.amount), + zatoshi: state.transaction.amount, to: state.transaction.toAddress, memo: state.transaction.memo, from: 0 @@ -172,7 +171,7 @@ extension SendFlowReducer { case .synchronizerStateChanged(.synced): return environment.SDKSynchronizer.getShieldedBalance() .receive(on: environment.scheduler) - .map({ $0.total }) + .map({ Zatoshi(amount: $0.total) }) .map(SendFlowAction.updateBalance) .eraseToEffect() @@ -181,7 +180,7 @@ extension SendFlowReducer { case .updateBalance(let balance): state.totalBalance = balance - state.transactionAmountInputState.maxValue = balance + state.transactionAmountInputState.maxValue = balance.amount return .none case .updateMemo(let memo): @@ -287,7 +286,7 @@ extension SendFlowState { .init( route: nil, transaction: .init( - amount: 0, + amount: Zatoshi.zero, memo: "", toAddress: "" ), @@ -309,11 +308,11 @@ extension SendFlowStore { ), reducer: .default, environment: SendFlowEnvironment( - mnemonic: .live, - scheduler: DispatchQueue.main.eraseToAnyScheduler(), - walletStorage: .live(), derivationTool: .live(), - SDKSynchronizer: LiveWrappedSDKSynchronizer() + mnemonic: .live, + SDKSynchronizer: LiveWrappedSDKSynchronizer(), + scheduler: DispatchQueue.main.eraseToAnyScheduler(), + walletStorage: .live() ) ) } diff --git a/secant/Features/SendFlow/SendFlowView.swift b/secant/Features/SendFlow/SendFlowView.swift index 263079a..e39a438 100644 --- a/secant/Features/SendFlow/SendFlowView.swift +++ b/secant/Features/SendFlow/SendFlowView.swift @@ -41,11 +41,11 @@ struct SendFLowView_Previews: PreviewProvider { ), reducer: .default, environment: SendFlowEnvironment( - mnemonic: .live, - scheduler: DispatchQueue.main.eraseToAnyScheduler(), - walletStorage: .live(), derivationTool: .live(), - SDKSynchronizer: LiveWrappedSDKSynchronizer() + mnemonic: .live, + SDKSynchronizer: LiveWrappedSDKSynchronizer(), + scheduler: DispatchQueue.main.eraseToAnyScheduler(), + walletStorage: .live() ) ) ) diff --git a/secant/Features/SendFlow/Views/CreateTransactionView.swift b/secant/Features/SendFlow/Views/CreateTransactionView.swift index 9ea5463..8ff29a5 100644 --- a/secant/Features/SendFlow/Views/CreateTransactionView.swift +++ b/secant/Features/SendFlow/Views/CreateTransactionView.swift @@ -10,8 +10,8 @@ struct CreateTransaction: View { return WithViewStore(store) { viewStore in VStack { VStack(spacing: 0) { - Text("Balance \(viewStore.totalBalance.asZecString()) ZEC") - Text("($\(viewStore.totalCurrencyBalance.asZecString()))") + Text("Balance \(viewStore.totalBalance.decimalString()) ZEC") + Text("($\(viewStore.totalCurrencyBalance.decimalString()))") .font(.system(size: 13)) .opacity(0.6) } @@ -32,9 +32,7 @@ struct CreateTransaction: View { Spacer() } - } - - if viewStore.isInsufficientFunds { + } else if viewStore.isInsufficientFunds { HStack { Text("insufficient funds") .foregroundColor(.red) diff --git a/secant/Features/SendFlow/Views/TransactionConfirmationView.swift b/secant/Features/SendFlow/Views/TransactionConfirmationView.swift index fa77aab..46e327d 100644 --- a/secant/Features/SendFlow/Views/TransactionConfirmationView.swift +++ b/secant/Features/SendFlow/Views/TransactionConfirmationView.swift @@ -6,7 +6,7 @@ struct TransactionConfirmation: View { var body: some View { VStack { - Text("Send \(viewStore.transaction.amount.asZecString()) ZEC") + Text("Send \(viewStore.transaction.amount.decimalString()) ZEC") .padding() Text("To \(viewStore.transaction.toAddress)") diff --git a/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowStore.swift b/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowStore.swift index d1959d8..f274fe3 100644 --- a/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowStore.swift +++ b/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowStore.swift @@ -101,7 +101,7 @@ extension TransactionState { id: "2", status: .paid(success: true), subtitle: "", - zecAmount: 25 + zecAmount: Zatoshi(amount: 25) ) } } @@ -153,7 +153,7 @@ extension IdentifiedArrayOf where Element == TransactionState { id: String($0), status: .paid(success: true), subtitle: "", - zecAmount: 25 + zecAmount: Zatoshi(amount: 25) ) } ) diff --git a/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowView.swift b/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowView.swift index 80660cb..69edd1a 100644 --- a/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowView.swift +++ b/secant/Features/TransactionHistoryFlow/TransactionHistoryFlowView.swift @@ -52,7 +52,7 @@ extension TransactionHistoryFlowView { Spacer() Text(transaction.status == .received ? "+" : "") - + Text("\(transaction.zecAmount.asZecString()) ZEC") + + Text("\(transaction.zecAmount.decimalString()) ZEC") } } .navigationLink( diff --git a/secant/Models/SendFlowTransaction.swift b/secant/Models/SendFlowTransaction.swift index 683cc26..d941747 100644 --- a/secant/Models/SendFlowTransaction.swift +++ b/secant/Models/SendFlowTransaction.swift @@ -9,7 +9,7 @@ import Foundation /// Simple model that holds data throughout the `SendFlow` feature struct SendFlowTransaction: Equatable { - var amount: Int64 + var amount: Zatoshi var memo: String var toAddress: String } @@ -17,7 +17,7 @@ struct SendFlowTransaction: Equatable { extension SendFlowTransaction { static var placeholder: Self { .init( - amount: 0, + amount: Zatoshi.zero, memo: "", toAddress: "" ) diff --git a/secant/Models/TransactionState.swift b/secant/Models/TransactionState.swift index 32ed372..67a617b 100644 --- a/secant/Models/TransactionState.swift +++ b/secant/Models/TransactionState.swift @@ -25,7 +25,7 @@ struct TransactionState: Equatable, Identifiable { var id: String var status: Status var subtitle: String - var zecAmount: Int64 + var zecAmount: Zatoshi } extension TransactionState { @@ -36,7 +36,7 @@ extension TransactionState { status = sent ? .paid(success: confirmedTransaction.minedHeight > 0) : .received subtitle = "sent" zAddress = confirmedTransaction.toAddress - zecAmount = (sent ? -Int64(confirmedTransaction.value) : Int64(confirmedTransaction.value)) + zecAmount = sent ? Zatoshi(amount: -Int64(confirmedTransaction.value)) : Zatoshi(amount: Int64(confirmedTransaction.value)) if let memo = confirmedTransaction.memo { self.memo = memo.asZcashTransactionMemo() } @@ -51,7 +51,7 @@ extension TransactionState { expirationHeight = pendingTransaction.expiryHeight subtitle = "pending" zAddress = pendingTransaction.toAddress - zecAmount = -Int64(pendingTransaction.value) + zecAmount = Zatoshi(amount: -Int64(pendingTransaction.value)) if let memo = pendingTransaction.memo { self.memo = memo.asZcashTransactionMemo() } @@ -64,7 +64,7 @@ extension TransactionState { extension TransactionState { static func placeholder( date: Date, - amount: Int64, + amount: Zatoshi, shielded: Bool = true, status: Status = .received, subtitle: String = "", @@ -80,14 +80,14 @@ extension TransactionState { id: uuid, status: status, subtitle: subtitle, - zecAmount: status == .received ? amount : -amount + zecAmount: status == .received ? amount : Zatoshi(amount: -amount.amount) ) } } struct TransactionStateMockHelper { var date: TimeInterval - var amount: Int64 + var amount: Zatoshi var shielded = true var status: TransactionState.Status = .received var subtitle = "cleared" diff --git a/secant/UI Components/TextFields/TransactionAddress/TransactionAddressTextFieldStore.swift b/secant/UI Components/TextFields/TransactionAddress/TransactionAddressTextFieldStore.swift index 9f5cc2e..ad9befe 100644 --- a/secant/UI Components/TextFields/TransactionAddress/TransactionAddressTextFieldStore.swift +++ b/secant/UI Components/TextFields/TransactionAddress/TransactionAddressTextFieldStore.swift @@ -17,8 +17,8 @@ typealias TransactionAddressTextFieldReducer = Reducer< typealias TransactionAddressTextFieldStore = Store struct TransactionAddressTextFieldState: Equatable { - var textFieldState: TCATextFieldState var isValidAddress = false + var textFieldState: TCATextFieldState } enum TransactionAddressTextFieldAction: Equatable { diff --git a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift index 92bfadb..af9eb45 100644 --- a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift +++ b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift @@ -59,11 +59,11 @@ struct TransactionAmountTextField_Previews: PreviewProvider { TransactionAmountTextField( store: TransactionAmountTextFieldStore( initialState: .init( + currencySelectionState: .init(currencyType: .usd), textFieldState: .init( validationType: .floatingPoint, text: "" - ), - currencySelectionState: .init(currencyType: .usd) + ) ), reducer: .default, environment: .init() diff --git a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift index b4bea23..05ddc0a 100644 --- a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift +++ b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift @@ -16,29 +16,12 @@ typealias TransactionAmountTextFieldReducer = Reducer< typealias TransactionAmountTextFieldStore = Store struct TransactionAmountTextFieldState: Equatable { - var textFieldState: TCATextFieldState + var amount: Int64 = 0 var currencySelectionState: CurrencySelectionState var maxValue: Int64 = 0 + var textFieldState: TCATextFieldState // TODO: - Get the ZEC price from the SDK, issue 311, https://github.com/zcash/secant-ios-wallet/issues/311 - var zecPrice = 140.0 - - var amount: Int64 { - switch currencySelectionState.currencyType { - case .zec: - return (textFieldState.text.doubleValue ?? 0.0).asZec() - case .usd: - return ((textFieldState.text.doubleValue ?? 0.0) / zecPrice).asZec() - } - } - - var maxCurrencyConvertedValue: Int64 { - switch currencySelectionState.currencyType { - case .zec: - return maxValue - case .usd: - return (maxValue.asHumanReadableZecBalance() * zecPrice).asZec() - } - } + var zecPrice = Decimal(140.0) var isMax: Bool { return amount == maxValue @@ -47,9 +30,10 @@ struct TransactionAmountTextFieldState: Equatable { enum TransactionAmountTextFieldAction: Equatable { case clearValue + case currencySelection(CurrencySelectionAction) case setMax case textField(TCATextFieldAction) - case currencySelection(CurrencySelectionAction) + case updateAmount } struct TransactionAmountTextFieldEnvironment: Equatable {} @@ -59,43 +43,65 @@ extension TransactionAmountTextFieldReducer { [ textFieldReducer, currencySelectionReducer, - maxOverride, - currencyUpdate + amountTextFieldReducer ] ) - static let maxOverride = TransactionAmountTextFieldReducer { state, action, _ in + static let amountTextFieldReducer = TransactionAmountTextFieldReducer { state, action, _ in switch action { case .setMax: - state.textFieldState.text = "\(state.maxCurrencyConvertedValue.asZecString())" + let maxValueAsZec = Decimal(state.maxValue) / Decimal(Zatoshi.Constants.oneZecInZatoshi) + let currencyType = state.currencySelectionState.currencyType + let maxCurrencyConvertedValue: NSDecimalNumber = currencyType == .zec ? + NSDecimalNumber(decimal: maxValueAsZec).roundedZec : + NSDecimalNumber(decimal: maxValueAsZec * state.zecPrice).roundedZec + + // TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312) + let decimalString = NumberFormatter.zcashNumberFormatter.string(from: maxCurrencyConvertedValue) ?? "" + + state.textFieldState.text = "\(decimalString)" + return Effect(value: .updateAmount) case .clearValue: state.textFieldState.text = "" + return .none + + case .textField(.set(let amount)): + return Effect(value: .updateAmount) - default: break - } - - return .none - } - - static let currencyUpdate = TransactionAmountTextFieldReducer { state, action, _ in - switch action { - case .currencySelection: - guard let currentDoubleValue = state.textFieldState.text.doubleValue else { + case .updateAmount: + // TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312) + guard var number = NumberFormatter.zcashNumberFormatter.number(from: state.textFieldState.text) else { + state.amount = 0 return .none } - + switch state.currencySelectionState.currencyType { + case .zec: + state.amount = NSDecimalNumber(decimal: number.decimalValue * Decimal(Zatoshi.Constants.oneZecInZatoshi)).roundedZec.int64Value + case .usd: + let decimal = (number.decimalValue / state.zecPrice) * Decimal(Zatoshi.Constants.oneZecInZatoshi) + state.amount = NSDecimalNumber(decimal: decimal).roundedZec.int64Value + } + return .none + + case .currencySelection: + // TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312) + guard let number = NumberFormatter.zcashNumberFormatter.number(from: state.textFieldState.text) else { + state.amount = 0 + return .none + } + let currencyType = state.currencySelectionState.currencyType - + let newValue = currencyType == .zec ? - currentDoubleValue / state.zecPrice : - currentDoubleValue * state.zecPrice - state.textFieldState.text = "\(newValue.asZecString())" - - default: break + number.decimalValue / state.zecPrice : + number.decimalValue * state.zecPrice + + // TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312) + let decimalString = NumberFormatter.zcashNumberFormatter.string(from: NSDecimalNumber(decimal: newValue)) ?? "" + state.textFieldState.text = "\(decimalString)" + return Effect(value: .updateAmount) } - - return .none } private static let textFieldReducer: TransactionAmountTextFieldReducer = TCATextFieldReducer.default.pullback( @@ -113,13 +119,13 @@ extension TransactionAmountTextFieldReducer { extension TransactionAmountTextFieldState { static let placeholder = TransactionAmountTextFieldState( - textFieldState: .placeholder, - currencySelectionState: CurrencySelectionState() + currencySelectionState: CurrencySelectionState(), + textFieldState: .placeholder ) static let amount = TransactionAmountTextFieldState( - textFieldState: .amount, - currencySelectionState: CurrencySelectionState() + currencySelectionState: CurrencySelectionState(), + textFieldState: .amount ) } diff --git a/secant/Utils/Double+Zcash.swift b/secant/Utils/Double+Zcash.swift deleted file mode 100644 index ccd8920..0000000 --- a/secant/Utils/Double+Zcash.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Double+Zcash.swift -// secant-testnet -// -// Created by Lukáš Korba on 06.05.2022. -// - -import Foundation - -// TODO: Improve with decimals and zatoshi type, issue #272 (https://github.com/zcash/secant-ios-wallet/issues/272) -extension Double { - func asZec() -> Int64 { - return Int64((self * 100_000_000).rounded()) - } - - func asZecString() -> String { - NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: self)) ?? "" - } -} diff --git a/secant/Utils/Int64+Zcash.swift b/secant/Utils/Int64+Zcash.swift deleted file mode 100644 index bd95c22..0000000 --- a/secant/Utils/Int64+Zcash.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Int64+Zcash.swift -// secant-testnet -// -// Created by Lukáš Korba on 15.04.2022. -// - -import Foundation - -// TODO: Improve with decimals and zatoshi type, issue #272 (https://github.com/zcash/secant-ios-wallet/issues/272) -extension Int64 { - func asHumanReadableZecBalance() -> Double { - Double(self) / Double(100_000_000) - } - - func asZecString() -> String { - NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: self.asHumanReadableZecBalance())) ?? "" - } -} diff --git a/secant/Utils/Zatoshi.swift b/secant/Utils/Zatoshi.swift index 5a96ed5..5a23988 100644 --- a/secant/Utils/Zatoshi.swift +++ b/secant/Utils/Zatoshi.swift @@ -14,6 +14,8 @@ struct Zatoshi { static let maxZatoshi: Int64 = Constants.oneZecInZatoshi * Constants.maxZecSupply } + static var zero: Zatoshi { Zatoshi() } + static var decimalHandler = NSDecimalNumberHandler( roundingMode: NSDecimalNumber.RoundingMode.bankers, scale: 8, @@ -32,8 +34,8 @@ struct Zatoshi { } /// Converts `Zatoshi` to human readable format, up to 8 fraction digits - var decimalString: String { - decimalValue.roundedZec.stringValue + func decimalString(formatter: NumberFormatter = NumberFormatter.zcashNumberFormatter) -> String { + formatter.string(from: decimalValue.roundedZec) ?? "" } /// Converts `Decimal` to `Zatoshi` @@ -61,14 +63,20 @@ struct Zatoshi { } } -extension NSDecimalNumber { - /// Converts `NSDecimalNumber` to human readable format, up to 8 fraction digits - var decimalString: String { - self.rounding(accordingToBehavior: Zatoshi.decimalHandler).stringValue +extension Zatoshi: Equatable { + static func == (lhs: Zatoshi, rhs: Zatoshi) -> Bool { + lhs.amount == rhs.amount } - +} + +extension NSDecimalNumber { /// Round the decimal to 8 fraction digits var roundedZec: NSDecimalNumber { self.rounding(accordingToBehavior: Zatoshi.decimalHandler) } + + /// Converts `NSDecimalNumber` to human readable format, up to 8 fraction digits + var decimalString: String { + self.roundedZec.stringValue + } } diff --git a/secant/Wrappers/WrappedSDKSynchronizer.swift b/secant/Wrappers/WrappedSDKSynchronizer.swift index 62dd595..5baced7 100644 --- a/secant/Wrappers/WrappedSDKSynchronizer.swift +++ b/secant/Wrappers/WrappedSDKSynchronizer.swift @@ -57,7 +57,7 @@ protocol WrappedSDKSynchronizer { func sendTransaction( with spendingKey: String, - zatoshi: Int64, + zatoshi: Zatoshi, to recipientAddress: String, memo: String?, from account: Int @@ -220,7 +220,7 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { func sendTransaction( with spendingKey: String, - zatoshi: Int64, + zatoshi: Zatoshi, to recipientAddress: String, memo: String?, from account: Int @@ -229,7 +229,7 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { Future { [weak self] promise in self?.synchronizer?.sendToAddress( spendingKey: spendingKey, - zatoshi: zatoshi, + zatoshi: zatoshi.amount, toAddress: recipientAddress, memo: memo, from: account) { result in @@ -301,11 +301,11 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { func getAllClearedTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039202, amount: 1, status: .paid(success: false)), - TransactionStateMockHelper(date: 1651039101, amount: 2), - TransactionStateMockHelper(date: 1651039000, amount: 3, status: .paid(success: true)), - TransactionStateMockHelper(date: 1651039505, amount: 4), - TransactionStateMockHelper(date: 1651039404, amount: 5) + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(amount: 1), status: .paid(success: false)), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(amount: 2)), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(amount: 3), status: .paid(success: true)), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(amount: 4)), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(amount: 5)) ] return Effect( @@ -324,10 +324,10 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { func getAllPendingTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039606, amount: 6, status: .paid(success: false), subtitle: "pending"), - TransactionStateMockHelper(date: 1651039303, amount: 7, subtitle: "pending"), - TransactionStateMockHelper(date: 1651039707, amount: 8, status: .paid(success: true), subtitle: "pending"), - TransactionStateMockHelper(date: 1651039808, amount: 9, subtitle: "pending") + TransactionStateMockHelper(date: 1651039606, amount: Zatoshi(amount: 6), status: .paid(success: false), subtitle: "pending"), + TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(amount: 7), subtitle: "pending"), + TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(amount: 8), status: .paid(success: true), subtitle: "pending"), + TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(amount: 9), subtitle: "pending") ] return Effect( @@ -360,7 +360,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { func sendTransaction( with spendingKey: String, - zatoshi: Int64, + zatoshi: Zatoshi, to recipientAddress: String, memo: String?, from account: Int @@ -375,7 +375,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { id: "id", status: .paid(success: true), subtitle: "sub", - zecAmount: 10 + zecAmount: Zatoshi(amount: 10) ) return Effect(value: Result.success(transactionState)) @@ -408,11 +408,11 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { func getAllClearedTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039202, amount: 1, status: .paid(success: false), uuid: "aa11"), - TransactionStateMockHelper(date: 1651039101, amount: 2, uuid: "bb22"), - TransactionStateMockHelper(date: 1651039000, amount: 3, status: .paid(success: true), uuid: "cc33"), - TransactionStateMockHelper(date: 1651039505, amount: 4, uuid: "dd44"), - TransactionStateMockHelper(date: 1651039404, amount: 5, uuid: "ee55") + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(amount: 1), status: .paid(success: false), uuid: "aa11"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(amount: 2), uuid: "bb22"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(amount: 3), status: .paid(success: true), uuid: "cc33"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(amount: 4), uuid: "dd44"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(amount: 5), uuid: "ee55") ] return Effect( @@ -432,10 +432,16 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { func getAllPendingTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039606, amount: 6, status: .paid(success: false), subtitle: "pending", uuid: "ff66"), - TransactionStateMockHelper(date: 1651039303, amount: 7, subtitle: "pending", uuid: "gg77"), - TransactionStateMockHelper(date: 1651039707, amount: 8, status: .paid(success: true), subtitle: "pending", uuid: "hh88"), - TransactionStateMockHelper(date: 1651039808, amount: 9, subtitle: "pending", uuid: "ii99") + TransactionStateMockHelper( + date: 1651039606, + amount: Zatoshi(amount: 6), + status: .paid(success: false), + subtitle: "pending", + uuid: "ff66" + ), + TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(amount: 7), subtitle: "pending", uuid: "gg77"), + TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(amount: 8), status: .paid(success: true), subtitle: "pending", uuid: "hh88"), + TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(amount: 9), subtitle: "pending", uuid: "ii99") ] return Effect( @@ -469,7 +475,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { func sendTransaction( with spendingKey: String, - zatoshi: Int64, + zatoshi: Zatoshi, to recipientAddress: String, memo: String?, from account: Int diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index 3fa9936..e4daec2 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -10,8 +10,6 @@ import XCTest import ComposableArchitecture import ZcashLightClientKit -// TODO: these tests will be updated with the Zatoshi/Balance representative once done, issue #272 https://github.com/zcash/secant-ios-wallet/issues/272 - // TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312) // swiftlint:disable type_body_length @@ -32,11 +30,11 @@ class SendTests: XCTestCase { let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: storage), derivationTool: .live(), - SDKSynchronizer: MockWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: MockWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: storage) ) let store = TestStore( @@ -64,7 +62,7 @@ class SendTests: XCTestCase { id: "id", status: .paid(success: true), subtitle: "sub", - zecAmount: 10 + zecAmount: Zatoshi(amount: 10) ) // check the success transaction to be received back @@ -88,11 +86,11 @@ class SendTests: XCTestCase { let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: storage), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: storage) ) let store = TestStore( @@ -127,11 +125,11 @@ class SendTests: XCTestCase { let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -169,11 +167,11 @@ class SendTests: XCTestCase { let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -185,17 +183,19 @@ class SendTests: XCTestCase { // Checks the computed property `isInvalidAmountFormat` which controls the error message to be shown on the screen // With empty input it must be false store.send(.transactionAmountInput(.textField(.set("")))) + + store.receive(.transactionAmountInput(.updateAmount)) } func testInvalidAddressFormatEmptyInput() throws { let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -226,20 +226,20 @@ class SendTests: XCTestCase { transactionAddressInputState: .placeholder, transactionAmountInputState: TransactionAmountTextFieldState( - textFieldState: .amount, currencySelectionState: CurrencySelectionState(), - maxValue: 501_300 + maxValue: 501_300, + textFieldState: .amount ) ) let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -257,9 +257,21 @@ class SendTests: XCTestCase { ) } + store.receive(.transactionAmountInput(.updateAmount)) { state in + state.transactionAmountInputState.amount = 501_299 + } + store.send(.transactionAmountInput(.textField(.set("0.00501301")))) { state in state.transactionAmountInputState.textFieldState.text = "0.00501301" state.transactionAmountInputState.textFieldState.valid = true + XCTAssertFalse( + state.isInsufficientFunds, + "Send Tests: `testFundsSufficiency` is expected to be false but it's \(state.isInsufficientFunds)" + ) + } + + store.receive(.transactionAmountInput(.updateAmount)) { state in + state.transactionAmountInputState.amount = 501_301 XCTAssertTrue( state.isInsufficientFunds, "Send Tests: `testFundsSufficiency` is expected to be true but it's \(state.isInsufficientFunds)" @@ -273,11 +285,11 @@ class SendTests: XCTestCase { let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -286,18 +298,18 @@ class SendTests: XCTestCase { environment: testEnvironment ) - try amountFormatTest("1.234", true, 123_400_000, store) - try amountFormatTest("1,234", true, 123_400_000_000, store) - try amountFormatTest("1 234", true, 123_400_000_000, store) - try amountFormatTest("1,234.567", true, 123_456_700_000, store) - try amountFormatTest("1.", true, 100_000_000, store) - try amountFormatTest("1..", false, 0, store) - try amountFormatTest("1,.", false, 0, store) - try amountFormatTest("1.,", false, 0, store) - try amountFormatTest("1,,", false, 0, store) - try amountFormatTest("1,23", false, 0, store) - try amountFormatTest("1 23", false, 0, store) - try amountFormatTest("1.2.3", false, 0, store) + try amountFormatTest("1.234", true, 123_400_000, false, store) + try amountFormatTest("1,234", true, 123_400_000_000, false, store) + try amountFormatTest("1 234", true, 123_400_000_000, true, store) + try amountFormatTest("1,234.567", true, 123_456_700_000, false, store) + try amountFormatTest("1.", true, 100_000_000, false, store) + try amountFormatTest("1..", false, 0, false, store) + try amountFormatTest("1,.", false, 0, true, store) + try amountFormatTest("1.,", false, 0, true, store) + try amountFormatTest("1,,", false, 0, true, store) + try amountFormatTest("1,23", false, 0, true, store) + try amountFormatTest("1 23", false, 0, true, store) + try amountFormatTest("1.2.3", false, 0, true, store) } func testValidForm() throws { @@ -308,24 +320,25 @@ class SendTests: XCTestCase { transactionAddressInputState: .placeholder, transactionAmountInputState: TransactionAmountTextFieldState( + amount: 501_301, + currencySelectionState: CurrencySelectionState(), + maxValue: 501_302, textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "0.00501301" - ), - currencySelectionState: CurrencySelectionState(), - maxValue: 501_302 + ) ) ) let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -353,24 +366,24 @@ class SendTests: XCTestCase { transactionAddressInputState: .placeholder, transactionAmountInputState: TransactionAmountTextFieldState( + currencySelectionState: CurrencySelectionState(), + maxValue: 501_300, textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "0.00501301" - ), - currencySelectionState: CurrencySelectionState(), - maxValue: 501_300 + ) ) ) let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -398,24 +411,24 @@ class SendTests: XCTestCase { transactionAddressInputState: .placeholder, transactionAmountInputState: TransactionAmountTextFieldState( + currencySelectionState: CurrencySelectionState(), + maxValue: 501_302, textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "0.00501301" - ), - currencySelectionState: CurrencySelectionState(), - maxValue: 501_302 + ) ) ) let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -443,24 +456,24 @@ class SendTests: XCTestCase { transactionAddressInputState: .placeholder, transactionAmountInputState: TransactionAmountTextFieldState( + currencySelectionState: CurrencySelectionState(), + maxValue: 501_302, textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "0.0.0501301" - ), - currencySelectionState: CurrencySelectionState(), - maxValue: 501_302 + ) ) ) let testScheduler = DispatchQueue.test let testEnvironment = SendFlowEnvironment( - mnemonic: .mock, - scheduler: testScheduler.eraseToAnyScheduler(), - walletStorage: .live(walletStorage: WalletStorage(secItem: .live)), derivationTool: .live(), - SDKSynchronizer: TestWrappedSDKSynchronizer() + mnemonic: .mock, + SDKSynchronizer: TestWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: WalletStorage(secItem: .live)) ) let store = TestStore( @@ -488,16 +501,20 @@ private extension SendTests { _ amount: String, _ expectedValidationResult: Bool, _ expectedAmount: Int64, + _ expectedToReceive: Bool, _ store: TestStore ) throws { store.send(.transactionAmountInput(.textField(.set(amount)))) { state in state.transactionAmountInputState.textFieldState.text = amount state.transactionAmountInputState.textFieldState.valid = expectedValidationResult - XCTAssertEqual( - expectedAmount, - state.transactionAmountInputState.amount, - "Send Tests: `amountFormatTest` expected amount is \(expectedAmount) but result is \(state.isInvalidAddressFormat)" - ) + } + + if expectedToReceive { + store.receive(.transactionAmountInput(.updateAmount)) + } else { + store.receive(.transactionAmountInput(.updateAmount)) { state in + state.transactionAmountInputState.amount = expectedAmount + } } } } diff --git a/secantTests/SendTests/TransactionAmountInputTests.swift b/secantTests/SendTests/TransactionAmountInputTests.swift index 9ed7c38..4a13170 100644 --- a/secantTests/SendTests/TransactionAmountInputTests.swift +++ b/secantTests/SendTests/TransactionAmountInputTests.swift @@ -9,8 +9,6 @@ import XCTest @testable import secant_testnet import ComposableArchitecture -// TODO: these tests will be updated with the Zatoshi/Balance representative once done, issue #272 https://github.com/zcash/secant-ios-wallet/issues/272 - // TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312) class TransactionAmountTextFieldTests: XCTestCase { @@ -20,13 +18,13 @@ class TransactionAmountTextFieldTests: XCTestCase { let store = TestStore( initialState: TransactionAmountTextFieldState( + currencySelectionState: CurrencySelectionState(), + maxValue: 501_301, textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "0.002" - ), - currencySelectionState: CurrencySelectionState(), - maxValue: 501_301 + ) ), reducer: TransactionAmountTextFieldReducer.default, environment: TransactionAmountTextFieldEnvironment() @@ -34,7 +32,10 @@ class TransactionAmountTextFieldTests: XCTestCase { store.send(.setMax) { state in state.textFieldState.text = "0.00501301" - XCTAssertEqual(501_301, state.amount, "AmountInput Tests: `testMaxValue` expected \(501_301) but received \(state.amount)") + } + + store.receive(.updateAmount) { state in + state.amount = 501_301 } } @@ -42,13 +43,13 @@ class TransactionAmountTextFieldTests: XCTestCase { let store = TestStore( initialState: TransactionAmountTextFieldState( + currencySelectionState: CurrencySelectionState(), + maxValue: 501_301, textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "0.002" - ), - currencySelectionState: CurrencySelectionState(), - maxValue: 501_301 + ) ), reducer: TransactionAmountTextFieldReducer.default, environment: TransactionAmountTextFieldEnvironment() @@ -60,21 +61,21 @@ class TransactionAmountTextFieldTests: XCTestCase { } } - func testZecUsdConvertedAmount() throws { - try XCTSkipUnless(Locale.current.regionCode == "US", "testZecUsdConvertedAmount is designed to test US locale only") + func testZec_to_UsdConvertedAmount() throws { + try XCTSkipUnless(Locale.current.regionCode == "US", "testZec_to_UsdConvertedAmount is designed to test US locale only") let store = TestStore( initialState: TransactionAmountTextFieldState( + currencySelectionState: + CurrencySelectionState( + currencyType: .zec + ), textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "1.0" ), - currencySelectionState: - CurrencySelectionState( - currencyType: .zec - ), zecPrice: 1000.0 ), reducer: TransactionAmountTextFieldReducer.default, @@ -84,29 +85,59 @@ class TransactionAmountTextFieldTests: XCTestCase { store.send(.currencySelection(.swapCurrencyType)) { state in state.textFieldState.text = "1,000" state.currencySelectionState.currencyType = .usd - XCTAssertEqual( - 100_000_000, - state.amount, - "AmountInput Tests: `testZecUsdConvertedAmount` expected \(100_000_000) but received \(state.amount)" - ) + } + + store.receive(.updateAmount) { state in + state.amount = 100_000_000 } } - func testUsdZecConvertedAmount() throws { - try XCTSkipUnless(Locale.current.regionCode == "US", "testUsdZecConvertedAmount is designed to test US locale only") + func testBigZecAmount_to_UsdConvertedAmount() throws { + try XCTSkipUnless(Locale.current.regionCode == "US", "testBigZecAmount_to_UsdConvertedAmount is designed to test US locale only") let store = TestStore( initialState: TransactionAmountTextFieldState( + currencySelectionState: + CurrencySelectionState( + currencyType: .zec + ), + textFieldState: + TCATextFieldState( + validationType: .floatingPoint, + text: "25000" + ), + zecPrice: 1000.0 + ), + reducer: TransactionAmountTextFieldReducer.default, + environment: TransactionAmountTextFieldEnvironment() + ) + + store.send(.currencySelection(.swapCurrencyType)) { state in + state.textFieldState.text = "25,000,000" + state.currencySelectionState.currencyType = .usd + } + + store.receive(.updateAmount) { state in + state.amount = 2_500_000_000_000 + } + } + + func testUsd_to_ZecConvertedAmount() throws { + try XCTSkipUnless(Locale.current.regionCode == "US", "testUsd_to_ZecConvertedAmount is designed to test US locale only") + + let store = TestStore( + initialState: + TransactionAmountTextFieldState( + currencySelectionState: + CurrencySelectionState( + currencyType: .usd + ), textFieldState: TCATextFieldState( validationType: .floatingPoint, text: "1 000" ), - currencySelectionState: - CurrencySelectionState( - currencyType: .usd - ), zecPrice: 1000.0 ), reducer: TransactionAmountTextFieldReducer.default, @@ -116,11 +147,10 @@ class TransactionAmountTextFieldTests: XCTestCase { store.send(.currencySelection(.swapCurrencyType)) { state in state.textFieldState.text = "1" state.currencySelectionState.currencyType = .zec - XCTAssertEqual( - 100_000_000, - state.amount, - "AmountInput Tests: `testZecUsdConvertedAmount` expected \(100_000_000) but received \(state.amount)" - ) + } + + store.receive(.updateAmount) { state in + state.amount = 100_000_000 } } @@ -130,16 +160,16 @@ class TransactionAmountTextFieldTests: XCTestCase { let store = TestStore( initialState: TransactionAmountTextFieldState( - textFieldState: - TCATextFieldState( - validationType: .floatingPoint, - text: "5" - ), currencySelectionState: CurrencySelectionState( currencyType: .usd ), maxValue: 100_000_000, + textFieldState: + TCATextFieldState( + validationType: .floatingPoint, + text: "5" + ), zecPrice: 1000.0 ), reducer: TransactionAmountTextFieldReducer.default, @@ -150,6 +180,14 @@ class TransactionAmountTextFieldTests: XCTestCase { state.textFieldState.text = "1 000" state.textFieldState.valid = true state.currencySelectionState.currencyType = .usd + XCTAssertFalse( + state.isMax, + "AmountInput Tests: `testIfAmountIsMax` is expected to be false but it's \(state.isMax)" + ) + } + + store.receive(.updateAmount) { state in + state.amount = 100_000_000 XCTAssertTrue( state.isMax, "AmountInput Tests: `testIfAmountIsMax` is expected to be true but it's \(state.isMax)" @@ -163,16 +201,16 @@ class TransactionAmountTextFieldTests: XCTestCase { let store = TestStore( initialState: TransactionAmountTextFieldState( - textFieldState: - TCATextFieldState( - validationType: .floatingPoint, - text: "5" - ), currencySelectionState: CurrencySelectionState( currencyType: .zec ), maxValue: 200_000_000, + textFieldState: + TCATextFieldState( + validationType: .floatingPoint, + text: "5" + ), zecPrice: 1000.0 ), reducer: TransactionAmountTextFieldReducer.default, @@ -181,11 +219,10 @@ class TransactionAmountTextFieldTests: XCTestCase { store.send(.setMax) { state in state.textFieldState.text = "2" - XCTAssertEqual( - 200_000_000, - state.maxCurrencyConvertedValue, - "AmountInput Tests: `testMaxZecValue` expected \(200_000_000) but received \(state.maxCurrencyConvertedValue)" - ) + } + + store.receive(.updateAmount) { state in + state.amount = 200_000_000 } } @@ -195,16 +232,16 @@ class TransactionAmountTextFieldTests: XCTestCase { let store = TestStore( initialState: TransactionAmountTextFieldState( - textFieldState: - TCATextFieldState( - validationType: .floatingPoint, - text: "5" - ), currencySelectionState: CurrencySelectionState( currencyType: .usd ), maxValue: 200_000_000, + textFieldState: + TCATextFieldState( + validationType: .floatingPoint, + text: "5" + ), zecPrice: 1000.0 ), reducer: TransactionAmountTextFieldReducer.default, @@ -213,11 +250,10 @@ class TransactionAmountTextFieldTests: XCTestCase { store.send(.setMax) { state in state.textFieldState.text = "2,000" - XCTAssertEqual( - 200_000_000_000, - state.maxCurrencyConvertedValue, - "AmountInput Tests: `testMaxUsdValue` expected \(200_000_000_000) but received \(state.maxCurrencyConvertedValue)" - ) + } + + store.receive(.updateAmount) { state in + state.amount = 200_000_000 } } } diff --git a/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift b/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift index 144df99..39129b6 100644 --- a/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift +++ b/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift @@ -38,15 +38,21 @@ class TransactionHistoryTests: XCTestCase { func testSynchronizerStateChanged2Synced() throws { let mocked: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039202, amount: 1, status: .paid(success: false), uuid: "aa11"), - TransactionStateMockHelper(date: 1651039101, amount: 2, uuid: "bb22"), - TransactionStateMockHelper(date: 1651039000, amount: 3, status: .paid(success: true), uuid: "cc33"), - TransactionStateMockHelper(date: 1651039505, amount: 4, uuid: "dd44"), - TransactionStateMockHelper(date: 1651039404, amount: 5, uuid: "ee55"), - TransactionStateMockHelper(date: 1651039606, amount: 6, status: .paid(success: false), subtitle: "pending", uuid: "ff66"), - TransactionStateMockHelper(date: 1651039303, amount: 7, subtitle: "pending", uuid: "gg77"), - TransactionStateMockHelper(date: 1651039707, amount: 8, status: .paid(success: true), subtitle: "pending", uuid: "hh88"), - TransactionStateMockHelper(date: 1651039808, amount: 9, subtitle: "pending", uuid: "ii99") + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(amount: 1), status: .paid(success: false), uuid: "aa11"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(amount: 2), uuid: "bb22"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(amount: 3), status: .paid(success: true), uuid: "cc33"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(amount: 4), uuid: "dd44"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(amount: 5), uuid: "ee55"), + TransactionStateMockHelper( + date: 1651039606, + amount: Zatoshi(amount: 6), + status: .paid(success: false), + subtitle: "pending", + uuid: "ff66" + ), + TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(amount: 7), subtitle: "pending", uuid: "gg77"), + TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(amount: 8), status: .paid(success: true), subtitle: "pending", uuid: "hh88"), + TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(amount: 9), subtitle: "pending", uuid: "ii99") ] let transactions = mocked.map { diff --git a/secantTests/UtilTests/ZatoshiTests.swift b/secantTests/UtilTests/ZatoshiTests.swift index 9f6ed26..9c0dfde 100644 --- a/secantTests/UtilTests/ZatoshiTests.swift +++ b/secantTests/UtilTests/ZatoshiTests.swift @@ -122,9 +122,9 @@ class ZatoshiTests: XCTestCase { // so we convert it to string, in that case we are prooving it to be rendered // to the user exactly the way we want XCTAssertEqual( - number.decimalString, + number.decimalString(), "1.42857143", - "Zatoshi tests: the value is expected to be 1.42857143 but it's \(number.decimalString)" + "Zatoshi tests: the value is expected to be 1.42857143 but it's \(number.decimalString())" ) } @@ -151,9 +151,9 @@ class ZatoshiTests: XCTestCase { func testStringToZatoshi() throws { if let number = Zatoshi.from(decimalString: "200.0") { XCTAssertEqual( - number.decimalString, + number.decimalString(), "200", - "Zatoshi tests: `testStringToZec` the value is expected to be 200 but it's \(number.decimalString)" + "Zatoshi tests: `testStringToZec` the value is expected to be 200 but it's \(number.decimalString())" ) } else { XCTFail("Zatoshi tests: `testStringToZatoshi` failed to convert number.")