From 64d509aedbc15e5df475978d4e947f2566b83722 Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Thu, 5 Jan 2023 20:07:25 +0100 Subject: [PATCH] [#514] Adopt Unified Addresses (#515) - UAs integrated to the Profile and Address details screen - Snapshot tests for the AddressDetailsView - Unit tests for the AddressDetailsStore --- secant.xcodeproj/project.pbxproj | 24 ++++++ .../SDKSynchronizerInterface.swift | 9 +-- .../SDKSynchronizer/SDKSynchronizerLive.swift | 4 + .../SDKSynchronizerMocks.swift | 8 ++ .../SDKSynchronizer/SDKSynchronizerTest.swift | 4 + .../AddressDetails/AddressDetailsStore.swift | 29 ++++++- .../AddressDetails/AddressDetailsView.swift | 33 +++++++- secant/Features/Profile/ProfileStore.swift | 22 ++--- secant/Features/Profile/ProfileView.swift | 6 +- .../AddressDetailsTests.swift | 80 +++++++++++++++++++ secantTests/ProfileTests/ProfileTests.swift | 11 ++- .../AddressDetailsSnapshotTests.swift | 29 +++++++ 12 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 secantTests/AddressDetailsTests/AddressDetailsTests.swift create mode 100644 secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 5778649..aa7c95d 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -112,6 +112,8 @@ 9E153A7529216EFB00112F41 /* UserDefaultsLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A7229216EFB00112F41 /* UserDefaultsLiveKey.swift */; }; 9E153A7629216EFB00112F41 /* UserDefaultsInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A7329216EFB00112F41 /* UserDefaultsInterface.swift */; }; 9E153A7729216EFB00112F41 /* UserDefaultsTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A7429216EFB00112F41 /* UserDefaultsTestKey.swift */; }; + 9E207C362966EC77003E2C9B /* AddressDetailsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E207C352966EC77003E2C9B /* AddressDetailsSnapshotTests.swift */; }; + 9E207C392966EF87003E2C9B /* AddressDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E207C382966EF87003E2C9B /* AddressDetailsTests.swift */; }; 9E2AC0FF27D8EC120042AA47 /* MnemonicSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9E2AC0FE27D8EC120042AA47 /* MnemonicSwift */; }; 9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */; }; 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */; }; @@ -406,6 +408,8 @@ 9E153A7229216EFB00112F41 /* UserDefaultsLiveKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsLiveKey.swift; sourceTree = ""; }; 9E153A7329216EFB00112F41 /* UserDefaultsInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsInterface.swift; sourceTree = ""; }; 9E153A7429216EFB00112F41 /* UserDefaultsTestKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsTestKey.swift; sourceTree = ""; }; + 9E207C352966EC77003E2C9B /* AddressDetailsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsSnapshotTests.swift; sourceTree = ""; }; + 9E207C382966EF87003E2C9B /* AddressDetailsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsTests.swift; sourceTree = ""; }; 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 = ""; }; @@ -687,6 +691,7 @@ isa = PBXGroup; children = ( 9E391162284E3ECF0073DD9A /* SnapshotTests */, + 9E207C372966EF6E003E2C9B /* AddressDetailsTests */, 9E94C61E28AA7DD5008256E9 /* BalanceBreakdownTests */, 9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */, 9E7CB6222874245400A02233 /* ProfileTests */, @@ -998,6 +1003,22 @@ path = UserDefaults; sourceTree = ""; }; + 9E207C342966EC60003E2C9B /* AddressDetailsSnapshotTests */ = { + isa = PBXGroup; + children = ( + 9E207C352966EC77003E2C9B /* AddressDetailsSnapshotTests.swift */, + ); + path = AddressDetailsSnapshotTests; + sourceTree = ""; + }; + 9E207C372966EF6E003E2C9B /* AddressDetailsTests */ = { + isa = PBXGroup; + children = ( + 9E207C382966EF87003E2C9B /* AddressDetailsTests.swift */, + ); + path = AddressDetailsTests; + sourceTree = ""; + }; 9E2DF99727CF704D00649636 /* ImportWallet */ = { isa = PBXGroup; children = ( @@ -1034,6 +1055,7 @@ 9E391162284E3ECF0073DD9A /* SnapshotTests */ = { isa = PBXGroup; children = ( + 9E207C342966EC60003E2C9B /* AddressDetailsSnapshotTests */, 9E94C62128AA7ECD008256E9 /* BalanceBreakdownSnapshotTests */, 9E9ECC8B28589E150099D5A2 /* HomeSnapshotTests */, 9E9ECC9328589E150099D5A2 /* ImportWalletSnapshotTests */, @@ -2193,9 +2215,11 @@ 0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */, 9E66122A287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift in Sources */, 9EAB4676285B5C7C002904A0 /* DeeplinkTests.swift in Sources */, + 9E207C362966EC77003E2C9B /* AddressDetailsSnapshotTests.swift in Sources */, 9E3911392848AD500073DD9A /* HomeTests.swift in Sources */, 9E9ECC9C28589E150099D5A2 /* OnboardingSnapshotTests.swift in Sources */, 9E9ECC9728589E150099D5A2 /* HomeSnapshotTests.swift in Sources */, + 9E207C392966EF87003E2C9B /* AddressDetailsTests.swift in Sources */, 9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */, 0DB4E0B12881F2DB00947B78 /* WalletBalance+testing.swift in Sources */, 9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */, diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift index 91bc143..bd7691f 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift @@ -63,6 +63,7 @@ protocol SDKSynchronizerClient { func getAllPendingTransactions() -> Effect<[WalletEvent], Never> func getAllTransactions() -> Effect<[WalletEvent], Never> + func getUnifiedAddress(account: Int) -> UnifiedAddress? func getTransparentAddress(account: Int) -> TransparentAddress? func getSaplingAddress(accountIndex: Int) async -> SaplingAddress? @@ -78,12 +79,4 @@ extension SDKSynchronizerClient { func start() throws { try start(retry: false) } - - func getTransparentAddress() -> TransparentAddress? { - getTransparentAddress(account: 0) - } - - func getSaplingAddress() async -> SaplingAddress? { - await getSaplingAddress(accountIndex: 0) - } } diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift index 474a710..29d1954 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift @@ -166,6 +166,10 @@ class LiveSDKSynchronizerClient: SDKSynchronizerClient { return .none } + func getUnifiedAddress(account: Int) -> UnifiedAddress? { + synchronizer?.getUnifiedAddress(accountIndex: account) + } + func getTransparentAddress(account: Int) -> TransparentAddress? { synchronizer?.getTransparentAddress(accountIndex: account) } diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift index b203dad..e533daa 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift @@ -111,6 +111,14 @@ class MockSDKSynchronizerClient: SDKSynchronizerClient { .eraseToEffect() } + func getUnifiedAddress(account: Int) -> UnifiedAddress? { + // swiftlint:disable line_length + try! UnifiedAddress( + encoding: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h", + network: .testnet + ) + } + func getTransparentAddress(account: Int) -> TransparentAddress? { nil } func getSaplingAddress(accountIndex account: Int) -> SaplingAddress? { diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift index 6c3ac90..540e827 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift @@ -48,6 +48,8 @@ class NoopSDKSynchronizer: SDKSynchronizerClient { func getAllTransactions() -> Effect<[WalletEvent], Never> { Effect(value: []) } + func getUnifiedAddress(account: Int) -> UnifiedAddress? { nil } + func getTransparentAddress(account: Int) -> TransparentAddress? { nil } func getSaplingAddress(accountIndex account: Int) -> SaplingAddress? { nil } @@ -159,6 +161,8 @@ class TestSDKSynchronizerClient: SDKSynchronizerClient { .eraseToEffect() } + func getUnifiedAddress(account: Int) -> UnifiedAddress? { nil } + func getTransparentAddress(account: Int) -> TransparentAddress? { nil } func getSaplingAddress(accountIndex account: Int) -> SaplingAddress? { diff --git a/secant/Features/AddressDetails/AddressDetailsStore.swift b/secant/Features/AddressDetails/AddressDetailsStore.swift index a9214a9..adcab57 100644 --- a/secant/Features/AddressDetails/AddressDetailsStore.swift +++ b/secant/Features/AddressDetails/AddressDetailsStore.swift @@ -7,22 +7,43 @@ import Foundation import ComposableArchitecture +import ZcashLightClientKit typealias AddressDetailsStore = Store struct AddressDetailsReducer: ReducerProtocol { - struct State: Equatable { } + struct State: Equatable { + var uAddress: UnifiedAddress? + + var unifiedAddress: String { + uAddress?.stringEncoded ?? "could not extract UA" + } + + var transparentAddress: String { + uAddress?.transparentReceiver()?.stringEncoded ?? "could not extract transparent receiver from UA" + } + + var saplingAddress: String { + uAddress?.saplingReceiver()?.stringEncoded ?? "could not extract sapling receiver from UA" + } + } enum Action: Equatable { - case copyToPastboard(String) + case copySaplingAddressToPastboard + case copyTransparentAddressToPastboard + case copyUnifiedAddressToPastboard } @Dependency(\.pasteboard) var pasteboard func reduce(into state: inout State, action: Action) -> ComposableArchitecture.EffectTask { switch action { - case .copyToPastboard(let value): - pasteboard.setString(value) + case .copySaplingAddressToPastboard: + pasteboard.setString(state.saplingAddress) + case .copyTransparentAddressToPastboard: + pasteboard.setString(state.transparentAddress) + case .copyUnifiedAddressToPastboard: + pasteboard.setString(state.unifiedAddress) } return .none } diff --git a/secant/Features/AddressDetails/AddressDetailsView.swift b/secant/Features/AddressDetails/AddressDetailsView.swift index a1efdc1..0ee8e1b 100644 --- a/secant/Features/AddressDetails/AddressDetailsView.swift +++ b/secant/Features/AddressDetails/AddressDetailsView.swift @@ -8,18 +8,43 @@ import SwiftUI import ComposableArchitecture -struct AddressDetails: View { +struct AddressDetailsView: View { let store: AddressDetailsStore var body: some View { - WithViewStore(store) { _ in - Text("Address Details") + WithViewStore(store) { viewStore in + VStack { + Text("Unified Address") + + Text("\(viewStore.unifiedAddress)") + .onTapGesture { + viewStore.send(.copyUnifiedAddressToPastboard) + } + + Text("Sapling Address") + .padding(.top, 20) + + Text("\(viewStore.saplingAddress)") + .onTapGesture { + viewStore.send(.copySaplingAddressToPastboard) + } + + Text("Transparent Address") + .padding(.top, 20) + + Text("\(viewStore.transparentAddress)") + .onTapGesture { + viewStore.send(.copyTransparentAddressToPastboard) + } + } + .padding(20) + .applyScreenBackground() } } } struct AddressDetails_Previews: PreviewProvider { static var previews: some View { - AddressDetails(store: .placeholder) + AddressDetailsView(store: .placeholder) } } diff --git a/secant/Features/Profile/ProfileStore.swift b/secant/Features/Profile/ProfileStore.swift index e3e66c7..e88d2ab 100644 --- a/secant/Features/Profile/ProfileStore.swift +++ b/secant/Features/Profile/ProfileStore.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import SwiftUI +import ZcashLightClientKit typealias ProfileStore = Store typealias ProfileViewStore = ViewStore @@ -11,20 +12,22 @@ struct ProfileReducer: ReducerProtocol { case settings } - var address = "" var addressDetailsState: AddressDetailsReducer.State var appBuild = "" var appVersion = "" var destination: Destination? var sdkVersion = "" var settingsState: SettingsReducer.State + + var unifiedAddress: String { + addressDetailsState.uAddress?.stringEncoded ?? "could not extract UA" + } } enum Action: Equatable { case addressDetails(AddressDetailsReducer.Action) case back case onAppear - case onAppearFinished(String) case settings(SettingsReducer.Action) case updateDestination(ProfileReducer.State.Destination?) } @@ -45,13 +48,7 @@ struct ProfileReducer: ReducerProtocol { Reduce { state, action in switch action { case .onAppear: - return Effect.task { - let saplingAddress = await self.sdkSynchronizer.getSaplingAddress()?.stringEncoded ?? "" - return .onAppearFinished(saplingAddress) - } - - case let .onAppearFinished(saplingAddress): - state.address = saplingAddress + state.addressDetailsState.uAddress = self.sdkSynchronizer.getUnifiedAddress(account: 0) state.appBuild = appVersion.appBuild() state.appVersion = appVersion.appVersion() state.sdkVersion = zcashSDKEnvironment.sdkVersion @@ -77,6 +74,13 @@ struct ProfileReducer: ReducerProtocol { // MARK: - Store extension ProfileStore { + func addressStore() -> AddressDetailsStore { + self.scope( + state: \.addressDetailsState, + action: ProfileReducer.Action.addressDetails + ) + } + func settingsStore() -> SettingsStore { self.scope( state: \.settingsState, diff --git a/secant/Features/Profile/ProfileView.swift b/secant/Features/Profile/ProfileView.swift index b370893..7fe9e5c 100644 --- a/secant/Features/Profile/ProfileView.swift +++ b/secant/Features/Profile/ProfileView.swift @@ -7,10 +7,10 @@ struct ProfileView: View { var body: some View { WithViewStore(store) { viewStore in VStack { - qrCodeUA(viewStore.address) + qrCodeUA(viewStore.unifiedAddress) .padding(.top, 30) - Text("Your UA address \(viewStore.address)") + Text("Your UA address \(viewStore.unifiedAddress)") .truncationMode(.middle) .multilineTextAlignment(.center) .lineLimit(2) @@ -73,7 +73,7 @@ struct ProfileView: View { .navigationLinkEmpty( isActive: viewStore.bindingForAddressDetails, destination: { - AddressDetails(store: .placeholder) + AddressDetailsView(store: store.addressStore()) } ) } diff --git a/secantTests/AddressDetailsTests/AddressDetailsTests.swift b/secantTests/AddressDetailsTests/AddressDetailsTests.swift new file mode 100644 index 0000000..cae8cae --- /dev/null +++ b/secantTests/AddressDetailsTests/AddressDetailsTests.swift @@ -0,0 +1,80 @@ +// +// AddressDetailsTests.swift +// secantTests +// +// Created by Lukáš Korba on 05.01.2023. +// + +import XCTest +@testable import secant_testnet +import ComposableArchitecture +import ZcashLightClientKit + +class AddressDetailsTests: XCTestCase { + // swiftlint:disable line_length + let uAddressEncoding = "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h" + + func testCopySaplingAddressToPasteboard() throws { + let testPasteboard = PasteboardClient.testPasteboard + let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) + + let store = TestStore( + initialState: AddressDetailsReducer.State(uAddress: uAddress), + reducer: AddressDetailsReducer() + ) { + $0.pasteboard = testPasteboard + } + + store.send(.copySaplingAddressToPastboard) + + let expectedAddress = uAddress.saplingReceiver()?.stringEncoded ?? "could not extract sapling receiver from UA" + + XCTAssertEqual( + testPasteboard.getString(), + expectedAddress, + "AddressDetails: `testCopySaplingAddressToPasteboard` is expected to match the input `\(expectedAddress)`" + ) + } + + func testCopyTransparentAddressToPasteboard() throws { + let testPasteboard = PasteboardClient.testPasteboard + let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) + + let store = TestStore( + initialState: AddressDetailsReducer.State(uAddress: uAddress), + reducer: AddressDetailsReducer() + ) { + $0.pasteboard = testPasteboard + } + + store.send(.copyTransparentAddressToPastboard) + + let expectedAddress = uAddress.transparentReceiver()?.stringEncoded ?? "could not extract transparent receiver from UA" + + XCTAssertEqual( + testPasteboard.getString(), + expectedAddress, + "AddressDetails: `testCopyTransparentAddressToPasteboard` is expected to match the input `\(expectedAddress)`" + ) + } + + func testCopyUnifiedAddressToPasteboard() throws { + let testPasteboard = PasteboardClient.testPasteboard + let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) + + let store = TestStore( + initialState: AddressDetailsReducer.State(uAddress: uAddress), + reducer: AddressDetailsReducer() + ) { + $0.pasteboard = testPasteboard + } + + store.send(.copyUnifiedAddressToPastboard) + + XCTAssertEqual( + testPasteboard.getString(), + uAddress.stringEncoded, + "AddressDetails: `testCopyUnifiedAddressToPasteboard` is expected to match the input `\(uAddress.stringEncoded)`" + ) + } +} diff --git a/secantTests/ProfileTests/ProfileTests.swift b/secantTests/ProfileTests/ProfileTests.swift index 7338428..9ebc738 100644 --- a/secantTests/ProfileTests/ProfileTests.swift +++ b/secantTests/ProfileTests/ProfileTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import secant_testnet import ComposableArchitecture +import ZcashLightClientKit class ProfileTests: XCTestCase { @MainActor func testSynchronizerStateChanged_AnyButSynced() async throws { @@ -19,10 +20,14 @@ class ProfileTests: XCTestCase { dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock } - _ = await store.send(.onAppear) + // swiftlint:disable line_length + let uAddress = try UnifiedAddress( + encoding: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h", + network: .testnet + ) - await store.receive(.onAppearFinished("ztestsapling1edm52k336nk70gxqxedd89slrrf5xwnnp5rt6gqnk0tgw4mynv6fcx42ym6x27yac5amvfvwypz")) { state in - state.address = "ztestsapling1edm52k336nk70gxqxedd89slrrf5xwnnp5rt6gqnk0tgw4mynv6fcx42ym6x27yac5amvfvwypz" + _ = await store.send(.onAppear) { state in + state.addressDetailsState.uAddress = uAddress state.appVersion = "0.0.1" state.appBuild = "31" state.sdkVersion = "0.17.0-beta" diff --git a/secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift b/secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift new file mode 100644 index 0000000..e1ddf23 --- /dev/null +++ b/secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift @@ -0,0 +1,29 @@ +// +// AddressDetailsSnapshotTests.swift +// secantTests +// +// Created by Lukáš Korba on 05.01.2023. +// + +import XCTest +@testable import secant_testnet +import ComposableArchitecture +import ZcashLightClientKit +import SwiftUI + +class AddressDetailsSnapshotTests: XCTestCase { + func testAddressDetailsSnapshot() throws { + // swiftlint:disable line_length + let uAddress = try UnifiedAddress( + encoding: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h", + network: .testnet + ) + + let store = Store( + initialState: AddressDetailsReducer.State(uAddress: uAddress), + reducer: AddressDetailsReducer() + ) + + addAttachments(AddressDetailsView(store: store)) + } +}