Initial functional code

cleanup + functional import wallet

app state check fix

synchronization wip

cleanup

cleanup + debug menus from the home

debug menus modifier added

fix for the failing tests

transparent -> shielded

comments resolved
This commit is contained in:
Lukas Korba 2022-04-13 15:52:31 +02:00
parent 7b33b632b9
commit c6a90ff96c
20 changed files with 604 additions and 379 deletions

View File

@ -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 = "<group>"; };
0D185818272723FF0046B928 /* ColoredChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredChip.swift; sourceTree = "<group>"; };
0D18581A272728D60046B928 /* PhraseChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhraseChip.swift; sourceTree = "<group>"; };
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = "<group>"; };
0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayReducerTests.swift; sourceTree = "<group>"; };
0D2ACE7F26C2C67100D62E3C /* Zboto.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Zboto.otf; sourceTree = "<group>"; };
0D354A0626D5A9D000315F45 /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
@ -254,6 +255,8 @@
9E2DF99827CF704D00649636 /* ImportWalletStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletStore.swift; sourceTree = "<group>"; };
9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportSeedEditor.swift; sourceTree = "<group>"; };
9E2DF99B27CF704D00649636 /* ImportWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletView.swift; sourceTree = "<group>"; };
9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int64+Zcash.swift"; sourceTree = "<group>"; };
9E2F1C832809B606004E65FE /* DebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugMenu.swift; sourceTree = "<group>"; };
9E37A2B727C8F59F00AE57B3 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = "<group>"; };
9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = "<group>"; };
@ -262,6 +265,8 @@
9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; };
9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItem.swift; sourceTree = "<group>"; };
9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItemTests.swift; sourceTree = "<group>"; };
9EAFEB872806E5AE00199FC9 /* CombineSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineSynchronizer.swift; sourceTree = "<group>"; };
9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZCashSDKEnvironment.swift; sourceTree = "<group>"; };
9EAFEB8D2808183D00199FC9 /* SandboxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxView.swift; sourceTree = "<group>"; };
9EAFEB8E2808183D00199FC9 /* SandboxStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxStore.swift; sourceTree = "<group>"; };
9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = "<group>"; };
@ -361,14 +366,6 @@
path = Screens;
sourceTree = "<group>";
};
0D1922F026BDE27D00052649 /* Stubs */ = {
isa = PBXGroup;
children = (
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */,
);
path = Stubs;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 */,

View File

@ -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<DispatchQueue>
let mnemonicSeedPhraseProvider: MnemonicSeedPhraseProvider
let scheduler: AnySchedulerOf<DispatchQueue>
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
)
}
}

View File

@ -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<HomeState, HomeAction, Void>
typealias HomeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
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<HomeState, HomeAction>
extension HomeState {
static var placeholder: Self {
.init(
balance: 1.2
totalBalance: 0.0,
verifiedBalance: 0.0
)
}
}

View File

@ -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()
)
)
}
}

View File

@ -6,6 +6,7 @@
//
import ComposableArchitecture
import ZcashLightClientKit
typealias ImportWalletStore = Store<ImportWalletState, ImportWalletAction>
@ -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()

View File

@ -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
)
}
)

View File

@ -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")

View File

@ -6,6 +6,7 @@
//
import Foundation
import ZcashLightClientKit
protocol Services {
var networkProvider: ZcashNetworkProvider { get }

View File

@ -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() {}
}

View File

@ -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<WalletBalance, Never> { 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<WalletBalance, Never>
init() {
self.shieldedBalance = CurrentValueSubject<WalletBalance, Never>(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<WalletBalance, Never>
init() {
self.shieldedBalance = CurrentValueSubject<WalletBalance, Never>(Balance(verified: 0, total: 0))
}
func prepareWith(initializer: Initializer) throws {
synchronizer = try SDKSynchronizer(initializer: initializer)
shieldedBalance = CurrentValueSubject<WalletBalance, Never>(
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)
}
}

View File

@ -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
},

View File

@ -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()
}
)
}
}

View File

@ -14,3 +14,7 @@ enum InitializationState: Equatable {
case filesMissing
case uninitialized
}
enum SDKInitializationError: Error {
case failed
}

View File

@ -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)
}
}

View File

@ -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.

View File

@ -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 {

View File

@ -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 }
)
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}