diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 51cd0bb..f72cf9b 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 0D0781C9278776D20083ACD7 /* ZcashSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0781C7278776D20083ACD7 /* ZcashSymbol.swift */; }; 0D185819272723FF0046B928 /* ColoredChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D185818272723FF0046B928 /* ColoredChip.swift */; }; 0D18581B272728D60046B928 /* PhraseChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18581A272728D60046B928 /* PhraseChip.swift */; }; - 0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */; }; 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */; }; 0D2ACE8026C2C67100D62E3C /* Zboto.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0D2ACE7F26C2C67100D62E3C /* Zboto.otf */; }; 0D354A0926D5A9D000315F45 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0626D5A9D000315F45 /* Services.swift */; }; @@ -32,7 +31,6 @@ 0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */; }; 0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */; }; 0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C5272B129C005A6414 /* WordChipGrid.swift */; }; - 0DA13CA526C1963000E3B610 /* Balance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DA13CA426C1963000E3B610 /* Balance.swift */; }; 0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DACFA7E27208CE00039EEA5 /* Clamped.swift */; }; 0DACFA8127208D940039EEA5 /* UInt+SuperscriptText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DACFA8027208D940039EEA5 /* UInt+SuperscriptText.swift */; }; 0DACFA9027209FA70039EEA5 /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8327209FA60039EEA5 /* Roboto-Bold.ttf */; }; @@ -96,6 +94,8 @@ 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 */; }; 9E37A2B827C8F59F00AE57B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */; }; 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */; }; 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; }; @@ -104,6 +104,8 @@ 9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; }; 9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */; }; 9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */; }; + 9EAFEB882806E5AE00199FC9 /* CombineSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB872806E5AE00199FC9 /* CombineSynchronizer.swift */; }; + 9EAFEB8A2806F48100199FC9 /* ZCashSDKEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */; }; 9EAFEB8F2808183D00199FC9 /* SandboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB8D2808183D00199FC9 /* SandboxView.swift */; }; 9EAFEB902808183D00199FC9 /* SandboxStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB8E2808183D00199FC9 /* SandboxStore.swift */; }; 9EAFEB9128081E9400199FC9 /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874ED273C4DE200F0E875 /* HomeStore.swift */; }; @@ -165,7 +167,6 @@ 0D0781C7278776D20083ACD7 /* ZcashSymbol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZcashSymbol.swift; sourceTree = ""; }; 0D185818272723FF0046B928 /* ColoredChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredChip.swift; sourceTree = ""; }; 0D18581A272728D60046B928 /* PhraseChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhraseChip.swift; sourceTree = ""; }; - 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = ""; }; 0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayReducerTests.swift; sourceTree = ""; }; 0D2ACE7F26C2C67100D62E3C /* Zboto.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Zboto.otf; sourceTree = ""; }; 0D354A0626D5A9D000315F45 /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = ""; }; @@ -254,6 +255,8 @@ 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 = ""; }; 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = ""; }; 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = ""; }; @@ -262,6 +265,8 @@ 9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = ""; }; 9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItem.swift; sourceTree = ""; }; 9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItemTests.swift; sourceTree = ""; }; + 9EAFEB872806E5AE00199FC9 /* CombineSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineSynchronizer.swift; sourceTree = ""; }; + 9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZCashSDKEnvironment.swift; sourceTree = ""; }; 9EAFEB8D2808183D00199FC9 /* SandboxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxView.swift; sourceTree = ""; }; 9EAFEB8E2808183D00199FC9 /* SandboxStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxStore.swift; sourceTree = ""; }; 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = ""; }; @@ -361,14 +366,6 @@ path = Screens; sourceTree = ""; }; - 0D1922F026BDE27D00052649 /* Stubs */ = { - isa = PBXGroup; - children = ( - 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */, - ); - path = Stubs; - sourceTree = ""; - }; 0D2ACE7E26C2C65E00D62E3C /* Fonts */ = { isa = PBXGroup; children = ( @@ -435,7 +432,6 @@ 0D2ACE7E26C2C65E00D62E3C /* Fonts */, 0DA13CA326C1960A00E3B610 /* Models */, 0DA13C9126C15E1900E3B610 /* UIComponents */, - 0D1922F026BDE27D00052649 /* Stubs */, 0D1922EB26BDD9A500052649 /* Screens */, 0D170A7426BC9B7500EB6A46 /* MockedDependencies */, 0D4E7A0826B364170058B01E /* SecantApp.swift */, @@ -559,6 +555,10 @@ 9EF8139B27F47AED0075AF48 /* InitializationState.swift */, 9EF8139027F191BF0075AF48 /* WalletStorageInteractor.swift */, 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */, + 9EAFEB872806E5AE00199FC9 /* CombineSynchronizer.swift */, + 9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */, + 9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */, + 9E2F1C832809B606004E65FE /* DebugMenu.swift */, ); path = Util; sourceTree = ""; @@ -1180,12 +1180,13 @@ 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 /* ValidationSucceededView.swift in Sources */, 2EB1C5E827D77F6100BC43D7 /* TextFieldStore.swift in Sources */, + 9EAFEB8A2806F48100199FC9 /* ZCashSDKEnvironment.swift in Sources */, 0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */, - 0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */, 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */, 0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */, 0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift in Sources */, @@ -1208,6 +1209,7 @@ 0D18581B272728D60046B928 /* PhraseChip.swift in Sources */, 0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */, 665C963F272C26E600BC04FB /* CircularFrameBackground.swift in Sources */, + 9EAFEB882806E5AE00199FC9 /* CombineSynchronizer.swift in Sources */, 0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */, F9971A4D27680DC400A2DB75 /* App.swift in Sources */, 9EAFEB9228081E9400199FC9 /* HomeView.swift in Sources */, @@ -1217,7 +1219,6 @@ 0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */, F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */, F9971A4E27680DC400A2DB75 /* AppView.swift in Sources */, - 0DA13CA526C1963000E3B610 /* Balance.swift in Sources */, 2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */, 66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */, 2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */, @@ -1235,6 +1236,7 @@ 66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */, 663FABA0271D876200E495F8 /* PrimaryButton.swift in Sources */, 663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */, + 9E2F1C842809B606004E65FE /* DebugMenu.swift in Sources */, 9E02B5C3280458D2005B809B /* WrappedDerivationTool.swift in Sources */, F9C165C02740403600592F76 /* ApproveView.swift in Sources */, 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */, diff --git a/secant/Features/App/App.swift b/secant/Features/App/App.swift index 49c38c0..7d85f2a 100644 --- a/secant/Features/App/App.swift +++ b/secant/Features/App/App.swift @@ -25,10 +25,11 @@ struct AppState: Equatable { enum AppAction: Equatable { case appDelegate(AppDelegateAction) + case checkBackupPhraseValidation case checkWalletInitialization case createNewWallet case home(HomeAction) - case initializeApp + case initializeSDK case nukeWallet case onboarding(OnboardingAction) case phraseDisplay(RecoveryPhraseDisplayAction) @@ -40,28 +41,34 @@ enum AppAction: Equatable { } struct AppEnvironment { + let combineSynchronizer: CombineSynchronizer let databaseFiles: DatabaseFilesInteractor - let scheduler: AnySchedulerOf let mnemonicSeedPhraseProvider: MnemonicSeedPhraseProvider + let scheduler: AnySchedulerOf let walletStorage: WalletStorageInteractor let wrappedDerivationTool: WrappedDerivationTool + let zcashSDKEnvironment: ZCashSDKEnvironment } extension AppEnvironment { static let live = AppEnvironment( + combineSynchronizer: LiveCombineSynchronizer(), databaseFiles: .live(), - scheduler: DispatchQueue.main.eraseToAnyScheduler(), mnemonicSeedPhraseProvider: .live, + scheduler: DispatchQueue.main.eraseToAnyScheduler(), walletStorage: .live(), - wrappedDerivationTool: .live() + wrappedDerivationTool: .live(), + zcashSDKEnvironment: .mainnet ) static let mock = AppEnvironment( + combineSynchronizer: LiveCombineSynchronizer(), databaseFiles: .live(), - scheduler: DispatchQueue.main.eraseToAnyScheduler(), mnemonicSeedPhraseProvider: .mock, + scheduler: DispatchQueue.main.eraseToAnyScheduler(), walletStorage: .live(), - wrappedDerivationTool: .live(derivationTool: DerivationTool(networkType: .testnet)) + wrappedDerivationTool: .live(derivationTool: DerivationTool(networkType: .mainnet)), + zcashSDKEnvironment: .mainnet ) } @@ -104,14 +111,17 @@ extension AppReducer { case .failed: // TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) state.appInitializationState = .failed - case .initialized: - return Effect(value: .initializeApp) case .keysMissing: // TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) state.appInitializationState = .keysMissing - case .filesMissing: - state.appInitializationState = .filesMissing - return Effect(value: .initializeApp) + case .initialized, .filesMissing: + if walletState == .filesMissing { + state.appInitializationState = .filesMissing + } + return .concatenate( + Effect(value: .initializeSDK), + Effect(value: .checkBackupPhraseValidation) + ) case .uninitialized: state.appInitializationState = .uninitialized return Effect(value: .updateRoute(.onboarding)) @@ -124,36 +134,54 @@ extension AppReducer { /// Stored wallet is present, database files may or may not be present, trying to initialize app state variables and environments. /// When initialization succeeds user is taken to the home screen. - case .initializeApp: + case .initializeSDK: do { state.storedWallet = try environment.walletStorage.exportWallet() + + guard let storedWallet = state.storedWallet else { + state.appInitializationState = .failed + // TODO: fatal error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) + return .none + } + + try environment.mnemonicSeedPhraseProvider.isValid(storedWallet.seedPhrase) + + let birthday = state.storedWallet?.birthday ?? environment.zcashSDKEnvironment.defaultBirthday + + let initializer = try prepareInitializer( + for: storedWallet.seedPhrase, + birthday: birthday, + with: environment + ) + try environment.combineSynchronizer.prepareWith(initializer: initializer) + try environment.combineSynchronizer.start() } catch { state.appInitializationState = .failed // TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) - return .none } - + return .none + + case .checkBackupPhraseValidation: guard let storedWallet = state.storedWallet else { - return Effect(value: .updateRoute(.onboarding)) - .delay(for: 3, scheduler: environment.scheduler) - .eraseToEffect() - .cancellable(id: ListenerId(), cancelInFlight: true) + state.appInitializationState = .failed + // TODO: fatal error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) + return .none } var landingRoute: AppState.Route = .home if !storedWallet.hasUserPassedPhraseBackupTest { - let phraseWords: [String] do { - phraseWords = try environment.mnemonicSeedPhraseProvider.asWords(storedWallet.seedPhrase) + let phraseWords = try environment.mnemonicSeedPhraseProvider.asWords(storedWallet.seedPhrase) + + let recoveryPhrase = RecoveryPhrase(words: phraseWords) + state.phraseDisplayState.phrase = recoveryPhrase + state.phraseValidationState = RecoveryPhraseValidationState.random(phrase: recoveryPhrase) + landingRoute = .phraseDisplay } catch { // TODO: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States return .none } - let recoveryPhrase = RecoveryPhrase(words: phraseWords) - state.phraseDisplayState.phrase = recoveryPhrase - state.phraseValidationState = RecoveryPhraseValidationState.random(phrase: recoveryPhrase) - landingRoute = .phraseDisplay } state.appInitializationState = .initialized @@ -161,25 +189,26 @@ extension AppReducer { .delay(for: 3, scheduler: environment.scheduler) .eraseToEffect() .cancellable(id: ListenerId(), cancelInFlight: true) - + case .createNewWallet: do { // get the random english mnemonic let randomPhrase = try environment.mnemonicSeedPhraseProvider.randomMnemonic() - let randomPhraseWords = try environment.mnemonicSeedPhraseProvider.asWords(randomPhrase) - // TODO: - Get birthday from the integrated SDK, issue 228 (https://github.com/zcash/secant-ios-wallet/issues/228) - // get the latest block height - let birthday = BlockHeight(12345678) + let birthday = try environment.zcashSDKEnvironment.lightWalletService.latestBlockHeight() // store the wallet to the keychain try environment.walletStorage.importWallet(randomPhrase, birthday, .english, false) // start the backup phrase validation test + let randomPhraseWords = try environment.mnemonicSeedPhraseProvider.asWords(randomPhrase) let recoveryPhrase = RecoveryPhrase(words: randomPhraseWords) state.phraseDisplayState.phrase = recoveryPhrase state.phraseValidationState = RecoveryPhraseValidationState.random(phrase: recoveryPhrase) - return Effect(value: .phraseValidation(.displayBackedUpPhrase)) + return .concatenate( + Effect(value: .initializeSDK), + Effect(value: .phraseValidation(.displayBackedUpPhrase)) + ) } catch { // TODO: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States } @@ -193,21 +222,28 @@ extension AppReducer { // TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) } return .none - + case .nukeWallet: environment.walletStorage.nukeWallet() - // TODO: - when DatabaseFiles dependency is merged, nukeFiles as well, issue #220 (https://github.com/zcash/secant-ios-wallet/issues/220) + do { + try environment.databaseFiles.nukeDbFilesFor(environment.zcashSDKEnvironment.network) + } catch { + // TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) + } return .none - - case .welcome(.debugMenuStartup): + + case .welcome(.debugMenuStartup), .home(.debugMenuStartup): return .concatenate( Effect.cancel(id: ListenerId()), Effect(value: .updateRoute(.startup)) ) - + case .onboarding(.importWallet(.successfullyRecovered)): - return Effect(value: .updateRoute(.sandbox)) - + return Effect(value: .updateRoute(.home)) + + case .onboarding(.importWallet(.initializeSDK)): + return Effect(value: .initializeSDK) + /// Default is meaningful here because there's `routeReducer` handling routes and this reducer is handling only actions. We don't here plenty of unused cases. default: return .none @@ -246,7 +282,11 @@ extension AppReducer { private static let homeReducer: AppReducer = HomeReducer.default.pullback( state: \AppState.homeState, action: /AppAction.home, - environment: { _ in } + environment: { environment in + HomeEnvironment( + combineSynchronizer: environment.combineSynchronizer + ) + } ) private static let onboardingReducer: AppReducer = OnboardingReducer.default.pullback( @@ -255,7 +295,8 @@ extension AppReducer { environment: { environment in OnboardingEnvironment( mnemonicSeedPhraseProvider: environment.mnemonicSeedPhraseProvider, - walletStorage: environment.walletStorage + walletStorage: environment.walletStorage, + zcashSDKEnvironment: environment.zcashSDKEnvironment ) } ) @@ -292,8 +333,9 @@ extension AppReducer { var keysPresent = false do { keysPresent = try environment.walletStorage.areKeysPresent() - // TODO: replace the hardcoded network with the environmental value, issue 239 (https://github.com/zcash/secant-ios-wallet/issues/239) - let databaseFilesPresent = try environment.databaseFiles.areDbFilesPresentFor("mainnet") + let databaseFilesPresent = try environment.databaseFiles.areDbFilesPresentFor( + environment.zcashSDKEnvironment.network + ) switch (keysPresent, databaseFilesPresent) { case (false, false): @@ -311,8 +353,9 @@ extension AppReducer { } } catch WalletStorage.WalletStorageError.uninitializedWallet { do { - // TODO: replace the hardcoded network with the environmental value, issue 239 (https://github.com/zcash/secant-ios-wallet/issues/239) - if try environment.databaseFiles.areDbFilesPresentFor("mainnet") { + if try environment.databaseFiles.areDbFilesPresentFor( + environment.zcashSDKEnvironment.network + ) { return .keysMissing } } catch { @@ -324,6 +367,35 @@ extension AppReducer { return .uninitialized } + + static func prepareInitializer( + for seedPhrase: String, + birthday: BlockHeight, + with environment: AppEnvironment + ) throws -> Initializer { + do { + let seedBytes = try environment.mnemonicSeedPhraseProvider.toSeed(seedPhrase) + let viewingKeys = try environment.wrappedDerivationTool.deriveUnifiedViewingKeysFromSeed(seedBytes, 1) + + let network = environment.zcashSDKEnvironment.network + + let initializer = Initializer( + cacheDbURL: try environment.databaseFiles.cacheDbURLFor(network), + dataDbURL: try environment.databaseFiles.dataDbURLFor(network), + pendingDbURL: try environment.databaseFiles.pendingDbURLFor(network), + endpoint: environment.zcashSDKEnvironment.endpoint, + network: environment.zcashSDKEnvironment.network, + spendParamsURL: try environment.databaseFiles.spendParamsURLFor(network), + outputParamsURL: try environment.databaseFiles.outputParamsURLFor(network), + viewingKeys: viewingKeys, + walletBirthday: birthday + ) + + return initializer + } catch { + throw SDKInitializationError.failed + } + } } // MARK: - AppStore @@ -335,7 +407,7 @@ extension AppStore { AppStore( initialState: .placeholder, reducer: .default, - environment: .mock + environment: .live ) } } diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index bae3f27..2dbddd5 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -2,19 +2,48 @@ import ComposableArchitecture import SwiftUI struct HomeState: Equatable { - var balance: Double + var totalBalance: Double + var verifiedBalance: Double + var arePublishersPrepared = false } enum HomeAction: Equatable { + case debugMenuStartup + case preparePublishers + case updateBalance(Balance) +} + +struct HomeEnvironment { + let combineSynchronizer: CombineSynchronizer } // MARK: - HomeReducer -typealias HomeReducer = Reducer +typealias HomeReducer = Reducer extension HomeReducer { - static let `default` = HomeReducer { _, _, _ in - return .none + static let `default` = HomeReducer { state, action, environment in + switch action { + case .preparePublishers: + if !state.arePublishersPrepared { + state.arePublishersPrepared = true + + return environment.combineSynchronizer.shieldedBalance + .receive(on: DispatchQueue.main) + .map({ Balance(verified: $0.verified, total: $0.total) }) + .map(HomeAction.updateBalance) + .eraseToEffect() + } + return .none + + case .updateBalance(let balance): + state.totalBalance = balance.total.asHumanReadableZecBalance() + state.verifiedBalance = balance.verified.asHumanReadableZecBalance() + return .none + + case .debugMenuStartup: + return .none + } } } @@ -26,7 +55,8 @@ typealias HomeStore = Store extension HomeState { static var placeholder: Self { .init( - balance: 1.2 + totalBalance: 0.0, + verifiedBalance: 0.0 ) } } diff --git a/secant/Features/Home/Views/HomeView.swift b/secant/Features/Home/Views/HomeView.swift index 280c050..6ed62d7 100644 --- a/secant/Features/Home/Views/HomeView.swift +++ b/secant/Features/Home/Views/HomeView.swift @@ -6,9 +6,14 @@ struct HomeView: View { var body: some View { WithViewStore(store) { viewStore in - VStack { - Text("balance \(viewStore.balance)") + VStack(alignment: .center, spacing: 30.0) { + Text("totalBalance \(viewStore.totalBalance)") + Text("verifiedBalance \(viewStore.verifiedBalance)") + .accessDebugMenuWithHiddenGesture { + viewStore.send(.debugMenuStartup) + } } + .onAppear(perform: { viewStore.send(.preparePublishers) }) } } } @@ -20,7 +25,9 @@ extension HomeStore { HomeStore( initialState: .placeholder, reducer: .default.debug(), - environment: () + environment: HomeEnvironment( + combineSynchronizer: LiveCombineSynchronizer() + ) ) } } diff --git a/secant/Features/ImportWallet/ImportWalletStore.swift b/secant/Features/ImportWallet/ImportWalletStore.swift index b3be11e..040ff03 100644 --- a/secant/Features/ImportWallet/ImportWalletStore.swift +++ b/secant/Features/ImportWallet/ImportWalletStore.swift @@ -6,6 +6,7 @@ // import ComposableArchitecture +import ZcashLightClientKit typealias ImportWalletStore = Store @@ -19,23 +20,27 @@ enum ImportWalletAction: Equatable, BindableAction { case dismissAlert case importRecoveryPhrase case importPrivateOrViewingKey + case initializeSDK case successfullyRecovered } struct ImportWalletEnvironment { let mnemonicSeedPhraseProvider: MnemonicSeedPhraseProvider let walletStorage: WalletStorageInteractor + let zcashSDKEnvironment: ZCashSDKEnvironment } extension ImportWalletEnvironment { static let live = ImportWalletEnvironment( mnemonicSeedPhraseProvider: .live, - walletStorage: .live() + walletStorage: .live(), + zcashSDKEnvironment: .mainnet ) static let demo = ImportWalletEnvironment( mnemonicSeedPhraseProvider: .mock, - walletStorage: .live() + walletStorage: .live(), + zcashSDKEnvironment: .testnet ) } @@ -57,15 +62,12 @@ extension ImportWalletReducer { try environment.mnemonicSeedPhraseProvider.isValid(state.importedSeedPhrase) // store it to the keychain - // TODO: - Get the latest block number, initialization of the SDK = Issue #239 (https://github.com/zcash/secant-ios-wallet/issues/239) - let birthday = BlockHeight(1386000) + let birthday = environment.zcashSDKEnvironment.defaultBirthday try environment.walletStorage.importWallet(state.importedSeedPhrase, birthday, .english, false) // update the backup phrase validation flag try environment.walletStorage.markUserPassedPhraseBackupTest() - // TODO: - Initialize the SDK with the new seed, initialization of the SDK = Issue #239 (https://github.com/zcash/secant-ios-wallet/issues/239) - // notify user // TODO: Proper Error/Success handling, issue 221 (https://github.com/zcash/secant-ios-wallet/issues/221) state.alert = AlertState( @@ -76,6 +78,8 @@ extension ImportWalletReducer { action: .send(.successfullyRecovered) ) ) + + return Effect(value: .initializeSDK) } catch { // TODO: Proper Error/Success handling, issue 221 (https://github.com/zcash/secant-ios-wallet/issues/221) state.alert = AlertState( @@ -95,6 +99,9 @@ extension ImportWalletReducer { case .successfullyRecovered: return Effect(value: .dismissAlert) + + case .initializeSDK: + return .none } } .binding() diff --git a/secant/Features/Onboarding/OnboardingStore.swift b/secant/Features/Onboarding/OnboardingStore.swift index 1c54f53..9389f6d 100644 --- a/secant/Features/Onboarding/OnboardingStore.swift +++ b/secant/Features/Onboarding/OnboardingStore.swift @@ -69,17 +69,20 @@ enum OnboardingAction: Equatable { struct OnboardingEnvironment { let mnemonicSeedPhraseProvider: MnemonicSeedPhraseProvider let walletStorage: WalletStorageInteractor + let zcashSDKEnvironment: ZCashSDKEnvironment } extension OnboardingEnvironment { static let live = OnboardingEnvironment( mnemonicSeedPhraseProvider: .live, - walletStorage: .live() + walletStorage: .live(), + zcashSDKEnvironment: .mainnet ) static let demo = OnboardingEnvironment( mnemonicSeedPhraseProvider: .mock, - walletStorage: .live() + walletStorage: .live(), + zcashSDKEnvironment: .testnet ) } @@ -139,7 +142,8 @@ extension OnboardingReducer { environment: { environment in ImportWalletEnvironment( mnemonicSeedPhraseProvider: environment.mnemonicSeedPhraseProvider, - walletStorage: environment.walletStorage + walletStorage: environment.walletStorage, + zcashSDKEnvironment: environment.zcashSDKEnvironment ) } ) diff --git a/secant/Features/Welcome/WelcomeView.swift b/secant/Features/Welcome/WelcomeView.swift index ab1c4c4..8d73a7a 100644 --- a/secant/Features/Welcome/WelcomeView.swift +++ b/secant/Features/Welcome/WelcomeView.swift @@ -10,43 +10,9 @@ import ComposableArchitecture struct WelcomeView: View { var store: WelcomeStore - - enum DragState { - case inactive - case pressing - case dragging(translation: CGSize) - } - - let topPaddingRatio: Double = 0.18 - let horizontalPaddingRatio: Double = 0.07 - - @GestureState var dragState = DragState.inactive - - var body: some View { - let longPressDrag = LongPressGesture(minimumDuration: 0.75) - .sequenced(before: DragGesture()) - .updating($dragState) { value, state, _ in - switch value { - // Long press begins. - case .first(true): - state = .pressing - // Long press confirmed, dragging may begin. - case .second(true, let drag): - state = .dragging(translation: drag?.translation ?? .zero) - // Dragging ended or the long press cancelled. - default: - state = .inactive - } - } - .onEnded { value in - guard case .second(true, let drag?) = value else { return } - - if drag.translation.height > 0 { - ViewStore(store).send(.debugMenuStartup) - } - } - return GeometryReader { proxy in + var body: some View { + GeometryReader { proxy in ZStack(alignment: .top) { VStack(alignment: .center, spacing: 80) { let diameter = proxy.size.width - 40 @@ -55,7 +21,9 @@ struct WelcomeView: View { width: diameter, height: diameter ) - .gesture(longPressDrag) + .accessDebugMenuWithHiddenGesture { + ViewStore(store).send(.debugMenuStartup) + } VStack { Text("welcomeScreen.title") diff --git a/secant/MockedDependencies/Services.swift b/secant/MockedDependencies/Services.swift index 58890a8..7ca873e 100644 --- a/secant/MockedDependencies/Services.swift +++ b/secant/MockedDependencies/Services.swift @@ -6,6 +6,7 @@ // import Foundation +import ZcashLightClientKit protocol Services { var networkProvider: ZcashNetworkProvider { get } diff --git a/secant/Stubs/ZcashSDKStubs.swift b/secant/Stubs/ZcashSDKStubs.swift deleted file mode 100644 index a01c73f..0000000 --- a/secant/Stubs/ZcashSDKStubs.swift +++ /dev/null @@ -1,237 +0,0 @@ -// -// ZcashSDKStubs.swift -// secant -// -// Created by Francisco Gindre on 8/6/21. -// - -import Foundation - -public typealias BlockHeight = Int - -public protocol ZcashNetwork { - var networkType: NetworkType { get } - var constants: NetworkConstants.Type { get } -} - -public enum NetworkType { - case mainnet - case testnet - - var networkId: UInt32 { - switch self { - case .mainnet: return 1 - case .testnet: return 0 - } - } -} - -extension NetworkType { - static func forChainName(_ chainame: String) -> NetworkType? { - switch chainame { - case "test": return .testnet - case "main": return .mainnet - default: return nil - } - } -} - -public enum ZcashNetworkBuilder { - public static func network(for networkType: NetworkType) -> ZcashNetwork { - switch networkType { - case .mainnet: return ZcashMainnet() - case .testnet: return ZcashTestnet() - } - } -} - -class ZcashTestnet: ZcashNetwork { - var networkType: NetworkType = .testnet - var constants: NetworkConstants.Type = ZcashSDKTestnetConstants.self -} - -class ZcashMainnet: ZcashNetwork { - var networkType: NetworkType = .mainnet - var constants: NetworkConstants.Type = ZcashSDKMainnetConstants.self -} - -/** -Constants of ZcashLightClientKit. -*/ -public enum ZcashSDK { - /** - The number of zatoshi that equal 1 ZEC. - */ - public static var zatoshiPerZEC: BlockHeight = 100_000_000 - - /** - The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design. - */ - public static var maxReorgSize = 100 - - /** - The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled by the rust backend but it is helpful to know what it is set to and should be kept in sync. - */ - public static var expiryOffset = 20 - - // - // Defaults - // - /** - Default size of batches of blocks to request from the compact block service. - */ - public static var defaultBatchSize = 100 - - /** - Default amount of time, in in seconds, to poll for new blocks. Typically, this should be about half the average block time. - */ - public static var defaultPollInterval: TimeInterval = 20 - - /** - Default attempts at retrying. - */ - public static var defaultRetrie: Int = 5 - - /** - The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer than this before retrying. - */ - public static var defaultMaxBackoffInterval: TimeInterval = 600 - - /** - Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from the reorg but smaller than the theoretical max reorg size of 100. - */ - public static var defaultRewindDistance: Int = 10 - - /** - The number of blocks to allow before considering our data to be stale. This usually helps with what to do when returning from the background and is exposed via the Synchronizer's isStale function. - */ - public static var defaultStaleTolerance: Int = 10 - - /** - Default Name for LibRustZcash data.db - */ - public static var defaultDataDbName = "data.db" - - /** - Default Name for Compact Block caches db - */ - public static var defaultCachesDbName = "caches.db" - - /** - Default name for pending transactions db - */ - public static var defaultPendingDbName = "pending.db" - - /** - File name for the sapling spend params - */ - public static var spendParamFileName = "sapling-spend.params" - - /** - File name for the sapling output params - */ - public static var outputParamFileName = "sapling-output.params" - - /** - The Url that is used by default in zcashd. We'll want to make this externally configurable, rather than baking it into the SDK but this will do for now, since we're using a cloudfront URL that already redirects. - */ - public static var cloudParamDirURL = "https://z.cash/downloads/" -} - -public protocol NetworkConstants { - /** - The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any block prior to this height, at all. - */ - static var saplingActivationHeight: BlockHeight { get } - - /** - Default Name for LibRustZcash data.db - */ - static var defaultDataDbName: String { get } - - /** - Default Name for Compact Block caches db - */ - static var defaultCachesDbName: String { get } - - /** - Default name for pending transactions db - */ - static var defaultPendingDbName: String { get } - - static var defaultDbNamePrefix: String { get } - - /** - Fixed height where the SDK considers that the ZIP-321 was deployed. This is a workaround for librustzcash not figuring out the tx fee from the tx itself. - */ - static var feeChangeHeight: BlockHeight { get } - - static func defaultFee(for height: BlockHeight) -> Int64 -} - -public extension NetworkConstants { - static func defaultFee(for height: BlockHeight = BlockHeight.max) -> Int64 { - guard height >= feeChangeHeight else { return 10_000 } - - return 1_000 - } -} - -public class ZcashSDKMainnetConstants: NetworkConstants { - /** - The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any blocks prior to this height, at all. - */ - public static var saplingActivationHeight: BlockHeight = 419_200 - - /** - Default Name for LibRustZcash data.db - */ - public static var defaultDataDbName = "data.db" - - /** - Default Name for Compact Block caches db - */ - public static var defaultCachesDbName = "caches.db" - - /** - Default name for pending transactions db - */ - public static var defaultPendingDbName = "pending.db" - - public static var defaultDbNamePrefix = "ZcashSdk_mainnet_" - - public static var feeChangeHeight: BlockHeight = 1_077_550 - - private init() {} -} - -public class ZcashSDKTestnetConstants: NetworkConstants { - /** - The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any blocks prior to this height, at all. - */ - public static var saplingActivationHeight: BlockHeight = 280_000 - - /** - Default Name for LibRustZcash data.db - */ - public static var defaultDataDbName = "data.db" - - /** - Default Name for Compact Block caches db - */ - public static var defaultCachesDbName = "caches.db" - - /** - Default name for pending transactions db - */ - public static var defaultPendingDbName = "pending.db" - - public static var defaultDbNamePrefix = "ZcashSdk_testnet_" - - /** - Estimated height where wallets are supposed to change the fee - */ - public static var feeChangeHeight: BlockHeight = 1_028_500 - - private init() {} -} diff --git a/secant/Util/CombineSynchronizer.swift b/secant/Util/CombineSynchronizer.swift new file mode 100644 index 0000000..3ee05ba --- /dev/null +++ b/secant/Util/CombineSynchronizer.swift @@ -0,0 +1,126 @@ +// +// CombineSynchronizer.swift +// secant-testnet +// +// Created by Lukáš Korba on 13.04.2022. +// + +import Foundation +import ZcashLightClientKit +import Combine + +struct Balance: WalletBalance, Equatable { + var verified: Int64 + var total: Int64 +} + +protocol CombineSynchronizer { + var synchronizer: SDKSynchronizer? { get } + var shieldedBalance: CurrentValueSubject { get } + + func prepareWith(initializer: Initializer) throws + func start(retry: Bool) throws + func stop() + func updatePublishers() + + func getTransparentAddress(account: Int) -> TransparentAddress? + func getShieldedAddress(account: Int) -> SaplingShieldedAddress? +} + +extension CombineSynchronizer { + func start() throws { + try start(retry: false) + } + + func getTransparentAddress() -> TransparentAddress? { + getTransparentAddress(account: 0) + } + + func getShieldedAddress() -> SaplingShieldedAddress? { + getShieldedAddress(account: 0) + } +} + +class LiveCombineSynchronizer: CombineSynchronizer { + private var cancellables: [AnyCancellable] = [] + private(set) var synchronizer: SDKSynchronizer? + private(set) var shieldedBalance: CurrentValueSubject + + init() { + self.shieldedBalance = CurrentValueSubject(Balance(verified: 0, total: 0)) + } + + func prepareWith(initializer: Initializer) throws { + synchronizer = try SDKSynchronizer(initializer: initializer) + + NotificationCenter.default.publisher(for: .synchronizerSynced) + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] _ in + self?.updatePublishers() + }) + .store(in: &cancellables) + + try synchronizer?.prepare() + } + + func start(retry: Bool) throws { + try synchronizer?.start(retry: retry) + } + + func stop() { + synchronizer?.stop() + } + + func updatePublishers() { + if let shieldedVerifiedBalance = synchronizer?.getShieldedVerifiedBalance(), + let shieldedTotalBalance = synchronizer?.getShieldedBalance(accountIndex: 0) { + shieldedBalance.send(Balance(verified: shieldedVerifiedBalance, total: shieldedTotalBalance)) + } else { + shieldedBalance.send(Balance(verified: 0, total: 0)) + } + } + + func getTransparentAddress(account: Int) -> TransparentAddress? { + synchronizer?.getTransparentAddress(accountIndex: account) + } + + func getShieldedAddress(account: Int) -> SaplingShieldedAddress? { + synchronizer?.getShieldedAddress(accountIndex: account) + } +} + +class MockCombineSynchronizer: CombineSynchronizer { + private(set) var synchronizer: SDKSynchronizer? + private(set) var shieldedBalance: CurrentValueSubject + + init() { + self.shieldedBalance = CurrentValueSubject(Balance(verified: 0, total: 0)) + } + + func prepareWith(initializer: Initializer) throws { + synchronizer = try SDKSynchronizer(initializer: initializer) + shieldedBalance = CurrentValueSubject( + Balance(verified: 0, total: 0) + ) + try synchronizer?.prepare() + } + + func start(retry: Bool) throws { + try synchronizer?.start(retry: retry) + } + + func stop() { + synchronizer?.stop() + } + + func updatePublishers() { + } + + func getTransparentAddress(account: Int) -> TransparentAddress? { + synchronizer?.getTransparentAddress(accountIndex: account) + } + + func getShieldedAddress(account: Int) -> SaplingShieldedAddress? { + synchronizer?.getShieldedAddress(accountIndex: account) + } +} diff --git a/secant/Util/DatabaseFiles.swift b/secant/Util/DatabaseFiles.swift index 9564c90..3877103 100644 --- a/secant/Util/DatabaseFiles.swift +++ b/secant/Util/DatabaseFiles.swift @@ -6,11 +6,16 @@ // import Foundation +import ZcashLightClientKit struct DatabaseFiles { enum DatabaseFilesError: Error { case getDocumentsURL + case getCacheURL case getDataURL + case getOutputParamsURL + case getPendingURL + case getSpendParamsURL case nukeFiles case filesPresentCheck } @@ -29,27 +34,83 @@ struct DatabaseFiles { } } - func dataDbURL(for network: String) throws -> URL { + func cacheDbURL(for network: ZcashNetwork) throws -> URL { do { - return try documentsDirectory().appendingPathComponent("zcash.\(network).data.db", isDirectory: false) + return try documentsDirectory() + .appendingPathComponent( + "\(network.constants.defaultDbNamePrefix)cache.db", + isDirectory: false + ) + } catch { + throw DatabaseFilesError.getCacheURL + } + } + + func dataDbURL(for network: ZcashNetwork) throws -> URL { + do { + return try documentsDirectory() + .appendingPathComponent( + "\(network.constants.defaultDbNamePrefix)data.db", + isDirectory: false + ) } catch { throw DatabaseFilesError.getDataURL } } - func areDbFilesPresent(for network: String) throws -> Bool { + func outputParamsURL(for network: ZcashNetwork) throws -> URL { do { - let dataDatabaseURL = try dataDbURL(for: network) - return fileManager.fileExists(dataDatabaseURL.path) + return try documentsDirectory() + .appendingPathComponent( + "\(network.constants.defaultDbNamePrefix)sapling-output.params", + isDirectory: false + ) + } catch { + throw DatabaseFilesError.getOutputParamsURL + } + } + + func pendingDbURL(for network: ZcashNetwork) throws -> URL { + do { + return try documentsDirectory() + .appendingPathComponent( + "\(network.constants.defaultDbNamePrefix)pending.db", + isDirectory: false + ) + } catch { + throw DatabaseFilesError.getPendingURL + } + } + + func spendParamsURL(for network: ZcashNetwork) throws -> URL { + do { + return try documentsDirectory() + .appendingPathComponent( + "\(network.constants.defaultDbNamePrefix)sapling-spend.params", + isDirectory: false + ) + } catch { + throw DatabaseFilesError.getSpendParamsURL + } + } + + func areDbFilesPresent(for network: ZcashNetwork) throws -> Bool { + do { + let dataDbURL = try dataDbURL(for: network) + return fileManager.fileExists(dataDbURL.path) } catch { throw DatabaseFilesError.filesPresentCheck } } - func nukeDbFiles(for network: String) throws { + func nukeDbFiles(for network: ZcashNetwork) throws { do { - let dataDatabaseURL = try dataDbURL(for: network) - try fileManager.removeItem(dataDatabaseURL) + let cacheDbURL = try cacheDbURL(for: network) + let dataDbURL = try dataDbURL(for: network) + let pendingDbURL = try pendingDbURL(for: network) + try fileManager.removeItem(cacheDbURL) + try fileManager.removeItem(dataDbURL) + try fileManager.removeItem(pendingDbURL) } catch { throw DatabaseFilesError.nukeFiles } @@ -58,9 +119,13 @@ struct DatabaseFiles { struct DatabaseFilesInteractor { let documentsDirectory: () throws -> URL - let dataDbURLFor: (String) throws -> URL - let areDbFilesPresentFor: (String) throws -> Bool - let nukeDbFilesFor: (String) throws -> Void + let cacheDbURLFor: (ZcashNetwork) throws -> URL + let dataDbURLFor: (ZcashNetwork) throws -> URL + let outputParamsURLFor: (ZcashNetwork) throws -> URL + let pendingDbURLFor: (ZcashNetwork) throws -> URL + let spendParamsURLFor: (ZcashNetwork) throws -> URL + let areDbFilesPresentFor: (ZcashNetwork) throws -> Bool + let nukeDbFilesFor: (ZcashNetwork) throws -> Void } extension DatabaseFilesInteractor { @@ -69,9 +134,21 @@ extension DatabaseFilesInteractor { documentsDirectory: { try databaseFiles.documentsDirectory() }, + cacheDbURLFor: { network in + try databaseFiles.cacheDbURL(for: network) + }, dataDbURLFor: { network in try databaseFiles.dataDbURL(for: network) }, + outputParamsURLFor: { network in + try databaseFiles.outputParamsURL(for: network) + }, + pendingDbURLFor: { network in + try databaseFiles.pendingDbURL(for: network) + }, + spendParamsURLFor: { network in + try databaseFiles.spendParamsURL(for: network) + }, areDbFilesPresentFor: { network in try databaseFiles.areDbFilesPresent(for: network) }, @@ -85,9 +162,21 @@ extension DatabaseFilesInteractor { documentsDirectory: { throw DatabaseFiles.DatabaseFilesError.getDocumentsURL }, + cacheDbURLFor: { _ in + throw DatabaseFiles.DatabaseFilesError.getCacheURL + }, dataDbURLFor: { _ in throw DatabaseFiles.DatabaseFilesError.getDataURL }, + outputParamsURLFor: { _ in + throw DatabaseFiles.DatabaseFilesError.getOutputParamsURL + }, + pendingDbURLFor: { _ in + throw DatabaseFiles.DatabaseFilesError.getPendingURL + }, + spendParamsURLFor: { _ in + throw DatabaseFiles.DatabaseFilesError.getSpendParamsURL + }, areDbFilesPresentFor: { _ in throw DatabaseFiles.DatabaseFilesError.filesPresentCheck }, diff --git a/secant/Util/DebugMenu.swift b/secant/Util/DebugMenu.swift new file mode 100644 index 0000000..728e864 --- /dev/null +++ b/secant/Util/DebugMenu.swift @@ -0,0 +1,60 @@ +// +// DebugMenu.swift +// secant-testnet +// +// Created by Lukáš Korba on 15.04.2022. +// + +import SwiftUI + +// TODO: Make sure this code will never be in the production (app store) build, issue 273 (https://github.com/zcash/secant-ios-wallet/issues/273) + +// swiftlint:disable:next private_over_fileprivate strict_fileprivate +fileprivate struct DebugMenuModifier: ViewModifier { + enum DragState { + case inactive + case pressing + case dragging(translation: CGSize) + } + + @GestureState var dragState = DragState.inactive + var minimumDuration: Double + let action: () -> Void + + func body(content: Content) -> some View { + let longPressDrag = LongPressGesture(minimumDuration: minimumDuration) + .sequenced(before: DragGesture()) + .updating($dragState) { value, state, _ in + switch value { + // Long press begins. + case .first(true): + state = .pressing + // Long press confirmed, dragging may begin. + case .second(true, let drag): + state = .dragging(translation: drag?.translation ?? .zero) + // Dragging ended or the long press cancelled. + default: + state = .inactive + } + } + .onEnded { value in + guard case .second(true, let drag?) = value else { return } + + if drag.translation.height > 0 { + action() + } + } + + return content.gesture(longPressDrag) + } +} + +extension View { + func accessDebugMenuWithHiddenGesture(minimumDuration: Double = 0.75, action: @escaping () -> Void ) -> some View { + self.modifier( + DebugMenuModifier(minimumDuration: minimumDuration) { + action() + } + ) + } +} diff --git a/secant/Util/InitializationState.swift b/secant/Util/InitializationState.swift index e0cc30b..683bc52 100644 --- a/secant/Util/InitializationState.swift +++ b/secant/Util/InitializationState.swift @@ -14,3 +14,7 @@ enum InitializationState: Equatable { case filesMissing case uninitialized } + +enum SDKInitializationError: Error { + case failed +} diff --git a/secant/Util/Int64+Zcash.swift b/secant/Util/Int64+Zcash.swift new file mode 100644 index 0000000..3d4b3c9 --- /dev/null +++ b/secant/Util/Int64+Zcash.swift @@ -0,0 +1,15 @@ +// +// 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) + } +} diff --git a/secant/Util/WalletStorage.swift b/secant/Util/WalletStorage.swift index 602ccde..ef64e76 100644 --- a/secant/Util/WalletStorage.swift +++ b/secant/Util/WalletStorage.swift @@ -7,6 +7,7 @@ import Foundation import MnemonicSwift +import ZcashLightClientKit /// Zcash implementation of the keychain that is not universal but designed to deliver functionality needed by the wallet itself. /// All the APIs should be thread safe according to official doc: @@ -35,6 +36,7 @@ struct WalletStorage { } private let secItem: WrappedSecItem + var zcashStoredWalletPrefix = "" init(secItem: WrappedSecItem) { self.secItem = secItem @@ -123,7 +125,7 @@ struct WalletStorage { throw KeychainError.encoding } - try updateData(data, forKey: WalletStorage.Constants.zcashStoredWallet) + try updateData(data, forKey: Constants.zcashStoredWallet) } catch { throw error } @@ -158,7 +160,7 @@ struct WalletStorage { func baseQuery(forAccount account: String = "", andKey forKey: String) -> [String: Any] { let query:[ String: AnyObject ] = [ /// Uniquely identify this keychain accessor - kSecAttrService as String: forKey as AnyObject, + kSecAttrService as String: (zcashStoredWalletPrefix + forKey) as AnyObject, kSecAttrAccount as String: account as AnyObject, kSecClass as String: kSecClassGenericPassword, /// The data in the keychain item can be accessed only while the device is unlocked by the user. diff --git a/secant/Util/WalletStorageInteractor.swift b/secant/Util/WalletStorageInteractor.swift index b273dca..614f4c7 100644 --- a/secant/Util/WalletStorageInteractor.swift +++ b/secant/Util/WalletStorageInteractor.swift @@ -7,6 +7,7 @@ import Foundation import MnemonicSwift +import ZcashLightClientKit /// Representation of the wallet stored in the persistent storage (typically keychain, handled by `WalletStorage`). struct StoredWallet: Codable, Equatable { diff --git a/secant/Util/ZCashSDKEnvironment.swift b/secant/Util/ZCashSDKEnvironment.swift new file mode 100644 index 0000000..10ec8c8 --- /dev/null +++ b/secant/Util/ZCashSDKEnvironment.swift @@ -0,0 +1,47 @@ +// +// ZCashSDKEnvironment.swift +// secant-testnet +// +// Created by Lukáš Korba on 13.04.2022. +// + +import Foundation +import ZcashLightClientKit + +// swiftlint:disable:next private_over_fileprivate strict_fileprivate +fileprivate enum ZcashSDKConstants { + static let endpointMainnetAddress = "lightwalletd.electriccoin.co" + static let endpointTestnetAddress = "lightwalletd.testnet.electriccoin.co" + static let endpointPort = 9067 + static let defaultBlockHeight = 1_629_724 +} + +struct ZCashSDKEnvironment { + let defaultBirthday: BlockHeight + let endpoint: LightWalletEndpoint + let lightWalletService: LightWalletService + let network: ZcashNetwork + let isMainnet: () -> Bool +} + +extension ZCashSDKEnvironment { + static let mainnet = ZCashSDKEnvironment( + defaultBirthday: BlockHeight(ZcashSDKConstants.defaultBlockHeight), + endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointMainnetAddress, port: ZcashSDKConstants.endpointPort), + lightWalletService: LightWalletGRPCService( + endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointMainnetAddress, port: ZcashSDKConstants.endpointPort) + ), + network: ZcashNetworkBuilder.network(for: .mainnet), + isMainnet: { true } + ) + + static let testnet = ZCashSDKEnvironment( + defaultBirthday: BlockHeight(ZcashSDKConstants.defaultBlockHeight), + endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointTestnetAddress, port: ZcashSDKConstants.endpointPort), + lightWalletService: LightWalletGRPCService( + endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointTestnetAddress, port: ZcashSDKConstants.endpointPort) + ), + network: ZcashNetworkBuilder.network(for: .testnet), + isMainnet: { false } + ) +} diff --git a/secantTests/AppReducer/AppReducerTests.swift b/secantTests/AppReducer/AppReducerTests.swift index 08e8873..226a1a8 100644 --- a/secantTests/AppReducer/AppReducerTests.swift +++ b/secantTests/AppReducer/AppReducerTests.swift @@ -13,20 +13,24 @@ class AppReducerTests: XCTestCase { static let testScheduler = DispatchQueue.test let testEnvironment = AppEnvironment( + combineSynchronizer: MockCombineSynchronizer(), databaseFiles: .throwing, - scheduler: testScheduler.eraseToAnyScheduler(), mnemonicSeedPhraseProvider: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), walletStorage: .throwing, - wrappedDerivationTool: .live() + wrappedDerivationTool: .live(), + zcashSDKEnvironment: .mainnet ) func testWalletInitializationState_Uninitialized() throws { let uninitializedEnvironment = AppEnvironment( + combineSynchronizer: MockCombineSynchronizer(), databaseFiles: .throwing, - scheduler: DispatchQueue.test.eraseToAnyScheduler(), mnemonicSeedPhraseProvider: .mock, + scheduler: DispatchQueue.test.eraseToAnyScheduler(), walletStorage: .throwing, - wrappedDerivationTool: .live() + wrappedDerivationTool: .live(), + zcashSDKEnvironment: .mainnet ) let walletState = AppReducer.walletInitializationState(uninitializedEnvironment) @@ -42,11 +46,13 @@ class AppReducerTests: XCTestCase { ) let keysMissingEnvironment = AppEnvironment( + combineSynchronizer: MockCombineSynchronizer(), databaseFiles: .live(databaseFiles: DatabaseFiles(fileManager: wfmMock)), - scheduler: Self.testScheduler.eraseToAnyScheduler(), mnemonicSeedPhraseProvider: .mock, + scheduler: Self.testScheduler.eraseToAnyScheduler(), walletStorage: .throwing, - wrappedDerivationTool: .live() + wrappedDerivationTool: .live(), + zcashSDKEnvironment: .mainnet ) let walletState = AppReducer.walletInitializationState(keysMissingEnvironment) @@ -62,11 +68,13 @@ class AppReducerTests: XCTestCase { ) let keysMissingEnvironment = AppEnvironment( + combineSynchronizer: MockCombineSynchronizer(), databaseFiles: .live(databaseFiles: DatabaseFiles(fileManager: wfmMock)), - scheduler: Self.testScheduler.eraseToAnyScheduler(), mnemonicSeedPhraseProvider: .mock, + scheduler: Self.testScheduler.eraseToAnyScheduler(), walletStorage: .throwing, - wrappedDerivationTool: .live() + wrappedDerivationTool: .live(), + zcashSDKEnvironment: .testnet ) let walletState = AppReducer.walletInitializationState(keysMissingEnvironment) @@ -118,7 +126,12 @@ class AppReducerTests: XCTestCase { state.appInitializationState = .filesMissing } - store.receive(.initializeApp) { state in + store.receive(.initializeSDK) { state in + // failed is expected because environment is throwing errors + state.appInitializationState = .failed + } + + store.receive(.checkBackupPhraseValidation) { state in // failed is expected because environment is throwing errors state.appInitializationState = .failed } @@ -133,7 +146,12 @@ class AppReducerTests: XCTestCase { store.send(.respondToWalletInitializationState(.initialized)) - store.receive(.initializeApp) { state in + store.receive(.initializeSDK) { state in + // failed is expected because environment is throwing errors + state.appInitializationState = .failed + } + + store.receive(.checkBackupPhraseValidation) { state in // failed is expected because environment is throwing errors state.appInitializationState = .failed } diff --git a/secantTests/Util/DatabaseFilesTests.swift b/secantTests/Util/DatabaseFilesTests.swift index 1422912..2baae8a 100644 --- a/secantTests/Util/DatabaseFilesTests.swift +++ b/secantTests/Util/DatabaseFilesTests.swift @@ -6,6 +6,7 @@ // import XCTest +import ZcashLightClientKit @testable import secant_testnet extension String: Error {} @@ -14,7 +15,11 @@ extension DatabaseFiles.DatabaseFilesError { var debugValue: String { switch self { case .getDocumentsURL: return "getDocumentsURL" + case .getCacheURL: return "getCacheURL" case .getDataURL: return "getDataURL" + case .getOutputParamsURL: return "getOutputParamsURL" + case .getPendingURL: return "getPendingURL" + case .getSpendParamsURL: return "getSpendParamsURL" case .nukeFiles: return "nukeFiles" case .filesPresentCheck: return "filesPresentCheck" } @@ -22,6 +27,8 @@ extension DatabaseFiles.DatabaseFilesError { } class DatabaseFilesTests: XCTestCase { + let network = ZcashNetworkBuilder.network(for: .testnet) + func testFailingDocumentsDirectory() throws { let mockedFileManager = WrappedFileManager( url: { _, _, _, _ in throw "some error" }, @@ -60,7 +67,7 @@ class DatabaseFilesTests: XCTestCase { let dfInteractor = DatabaseFilesInteractor.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager)) do { - _ = try dfInteractor.dataDbURLFor("") + _ = try dfInteractor.dataDbURLFor(network) XCTFail("DatabaseFiles: `testFailingDataDbURL` expected to fail but passed with no error.") } catch { @@ -88,7 +95,7 @@ class DatabaseFilesTests: XCTestCase { let dfInteractor = DatabaseFilesInteractor.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager)) do { - let areFilesPresent = try dfInteractor.areDbFilesPresentFor("") + let areFilesPresent = try dfInteractor.areDbFilesPresentFor(network) XCTAssertTrue(areFilesPresent, "DatabaseFiles: `testDatabaseFilesPresent` is expected to be true but it's \(areFilesPresent)") } catch { @@ -106,7 +113,7 @@ class DatabaseFilesTests: XCTestCase { let dfInteractor = DatabaseFilesInteractor.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager)) do { - let areFilesPresent = try dfInteractor.areDbFilesPresentFor("") + let areFilesPresent = try dfInteractor.areDbFilesPresentFor(network) XCTAssertFalse(areFilesPresent, "DatabaseFiles: `testDatabaseFilesNotPresent` is expected to be false but it's \(areFilesPresent)") } catch { @@ -124,7 +131,7 @@ class DatabaseFilesTests: XCTestCase { let dfInteractor = DatabaseFilesInteractor.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager)) do { - _ = try dfInteractor.areDbFilesPresentFor("") + _ = try dfInteractor.areDbFilesPresentFor(network) XCTFail("DatabaseFiles: `testDatabaseFilesPresentFailure` expected to fail but passed with no error.") } catch { @@ -152,7 +159,7 @@ class DatabaseFilesTests: XCTestCase { let dfInteractor = DatabaseFilesInteractor.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager)) do { - _ = try dfInteractor.nukeDbFilesFor("") + _ = try dfInteractor.nukeDbFilesFor(network) XCTFail("DatabaseFiles: `testNukeFiles_RemoveFileFailure` expected to fail but passed with no error.") } catch { @@ -180,7 +187,7 @@ class DatabaseFilesTests: XCTestCase { let dfInteractor = DatabaseFilesInteractor.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager)) do { - _ = try dfInteractor.nukeDbFilesFor("") + _ = try dfInteractor.nukeDbFilesFor(network) XCTFail("DatabaseFiles: `testNukeFiles_URLFailure` expected to fail but passed with no error.") } catch { diff --git a/secantTests/Util/WalletStorageTests.swift b/secantTests/Util/WalletStorageTests.swift index 3a8e46e..fb26b28 100644 --- a/secantTests/Util/WalletStorageTests.swift +++ b/secantTests/Util/WalletStorageTests.swift @@ -7,6 +7,7 @@ import XCTest import MnemonicSwift +import ZcashLightClientKit @testable import secant_testnet extension WalletStorage.WalletStorageError { @@ -26,9 +27,10 @@ class WalletStorageTests: XCTestCase { let seedPhrase = "one two three" let language = MnemonicLanguageType.english var storage = WalletStorage(secItem: .live) - + override func setUp() { super.setUp() + storage.zcashStoredWalletPrefix = "test_" deleteData(forKey: WalletStorage.Constants.zcashStoredWallet) }