[#654] Convert SDKSynchronizerDependency to regular TCA dependency (#672)

Closes #654

- `SDKSynchronizerClient` is now regular TCA dependency like any other
  dependency that we have in the app.
This commit is contained in:
Michal Fousek 2023-03-15 09:53:16 +01:00 committed by GitHub
parent 69c1aa12a3
commit 5ce87b18a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 628 additions and 1125 deletions

View File

@ -185,7 +185,6 @@
0D26AF4E299E8196005260EE /* BalanceBreakdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F5289BC58C00A6796F /* BalanceBreakdownView.swift */; };
0D26AF4F299E8196005260EE /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9971A6427680DFE00A2DB75 /* SettingsView.swift */; };
0D26AF50299E8196005260EE /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41EA273B50520021B49A /* Strings.swift */; };
0D26AF51299E8196005260EE /* SDKSynchronizerMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB863CE2923CA32003D0F8B /* SDKSynchronizerMocks.swift */; };
0D26AF52299E8196005260EE /* LogsHandlerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C7529880FC900D09B09 /* LogsHandlerTest.swift */; };
0D26AF53299E8196005260EE /* TextFieldFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */; };
0D26AF54299E8196005260EE /* CrashReportingInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D26103B298C3E4800CC9DE9 /* CrashReportingInterface.swift */; };
@ -502,7 +501,6 @@
9EB863C92923C953003D0F8B /* UserPreferencesStorageMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB863C82923C953003D0F8B /* UserPreferencesStorageMocks.swift */; };
9EB863CB2923CA20003D0F8B /* SDKSynchronizerLive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB863CA2923CA20003D0F8B /* SDKSynchronizerLive.swift */; };
9EB863CD2923CA28003D0F8B /* SDKSynchronizerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB863CC2923CA28003D0F8B /* SDKSynchronizerTest.swift */; };
9EB863D02923D3FC003D0F8B /* SDKSynchronizerMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB863CE2923CA32003D0F8B /* SDKSynchronizerMocks.swift */; };
9EBDF947291D75B2000A1A05 /* DiskSpaceCheckerInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBDF946291D75B2000A1A05 /* DiskSpaceCheckerInterface.swift */; };
9EBDF949291D75BF000A1A05 /* DiskSpaceCheckerLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBDF948291D75BF000A1A05 /* DiskSpaceCheckerLiveKey.swift */; };
9EBDF94B291D75C7000A1A05 /* DiskSpaceCheckerTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBDF94A291D75C7000A1A05 /* DiskSpaceCheckerTestKey.swift */; };
@ -821,7 +819,6 @@
9EB863C82923C953003D0F8B /* UserPreferencesStorageMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageMocks.swift; sourceTree = "<group>"; };
9EB863CA2923CA20003D0F8B /* SDKSynchronizerLive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKSynchronizerLive.swift; sourceTree = "<group>"; };
9EB863CC2923CA28003D0F8B /* SDKSynchronizerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKSynchronizerTest.swift; sourceTree = "<group>"; };
9EB863CE2923CA32003D0F8B /* SDKSynchronizerMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKSynchronizerMocks.swift; sourceTree = "<group>"; };
9EBDF946291D75B2000A1A05 /* DiskSpaceCheckerInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskSpaceCheckerInterface.swift; sourceTree = "<group>"; };
9EBDF948291D75BF000A1A05 /* DiskSpaceCheckerLiveKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskSpaceCheckerLiveKey.swift; sourceTree = "<group>"; };
9EBDF94A291D75C7000A1A05 /* DiskSpaceCheckerTestKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskSpaceCheckerTestKey.swift; sourceTree = "<group>"; };
@ -1980,7 +1977,6 @@
9EAFEB872806E5AE00199FC9 /* SDKSynchronizerInterface.swift */,
9EB863CA2923CA20003D0F8B /* SDKSynchronizerLive.swift */,
9EB863CC2923CA28003D0F8B /* SDKSynchronizerTest.swift */,
9EB863CE2923CA32003D0F8B /* SDKSynchronizerMocks.swift */,
);
path = SDKSynchronizer;
sourceTree = "<group>";
@ -2782,7 +2778,6 @@
0D26AF4E299E8196005260EE /* BalanceBreakdownView.swift in Sources */,
0D26AF4F299E8196005260EE /* SettingsView.swift in Sources */,
0D26AF50299E8196005260EE /* Strings.swift in Sources */,
0D26AF51299E8196005260EE /* SDKSynchronizerMocks.swift in Sources */,
0D26AF52299E8196005260EE /* LogsHandlerTest.swift in Sources */,
0D26AF53299E8196005260EE /* TextFieldFooter.swift in Sources */,
0D26AF54299E8196005260EE /* CrashReportingInterface.swift in Sources */,
@ -3010,7 +3005,6 @@
9E6713F8289BC58C00A6796F /* BalanceBreakdownView.swift in Sources */,
F9971A6627680DFE00A2DB75 /* SettingsView.swift in Sources */,
F96B41EB273B50520021B49A /* Strings.swift in Sources */,
9EB863D02923D3FC003D0F8B /* SDKSynchronizerMocks.swift in Sources */,
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */,
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
0D26103C298C3E4800CC9DE9 /* CrashReportingInterface.swift in Sources */,

View File

@ -27,93 +27,57 @@ struct DatabaseFiles {
self.fileManager = fileManager
}
func documentsDirectory() throws -> URL {
func documentsDirectory() -> URL {
do {
return try fileManager.url(.documentDirectory, .userDomainMask, nil, true)
} catch {
throw DatabaseFilesError.getDocumentsURL
// This is not super clean but this is second best thing when the above call fails.
return URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Documents")
}
}
func cacheDbURL(for network: ZcashNetwork) throws -> URL {
do {
return try documentsDirectory()
.appendingPathComponent(
"\(network.constants.defaultDbNamePrefix)cache.db",
isDirectory: false
func cacheDbURL(for network: ZcashNetwork) -> URL {
return documentsDirectory()
.appendingPathComponent(
"\(network.constants.defaultDbNamePrefix)cache.db",
isDirectory: false
)
}
func dataDbURL(for network: ZcashNetwork) -> URL {
return documentsDirectory()
.appendingPathComponent(
"\(network.constants.defaultDbNamePrefix)data.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 outputParamsURL(for network: ZcashNetwork) -> URL {
return documentsDirectory()
.appendingPathComponent(
"\(network.constants.defaultDbNamePrefix)sapling-output.params",
isDirectory: false
)
}
func outputParamsURL(for network: ZcashNetwork) throws -> URL {
do {
return try documentsDirectory()
.appendingPathComponent(
"\(network.constants.defaultDbNamePrefix)sapling-output.params",
isDirectory: false
)
} catch {
throw DatabaseFilesError.getOutputParamsURL
}
func pendingDbURL(for network: ZcashNetwork) -> URL {
return documentsDirectory()
.appendingPathComponent(
"\(network.constants.defaultDbNamePrefix)pending.db",
isDirectory: false
)
}
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) -> URL {
return documentsDirectory()
.appendingPathComponent(
"\(network.constants.defaultDbNamePrefix)sapling-spend.params",
isDirectory: false
)
}
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: ZcashNetwork) throws {
do {
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
}
func areDbFilesPresent(for network: ZcashNetwork) -> Bool {
let dataDbURL = dataDbURL(for: network)
return fileManager.fileExists(dataDbURL.path)
}
}

View File

@ -17,13 +17,12 @@ extension DependencyValues {
}
struct DatabaseFilesClient {
let documentsDirectory: () throws -> URL
let fsBlockDbRootFor: (ZcashNetwork) throws -> URL
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
var areDbFilesPresentFor: (ZcashNetwork) throws -> Bool
let nukeDbFilesFor: (ZcashNetwork) throws -> Void
let documentsDirectory: () -> URL
let fsBlockDbRootFor: (ZcashNetwork) -> URL
let cacheDbURLFor: (ZcashNetwork) -> URL
let dataDbURLFor: (ZcashNetwork) -> URL
let outputParamsURLFor: (ZcashNetwork) -> URL
let pendingDbURLFor: (ZcashNetwork) -> URL
let spendParamsURLFor: (ZcashNetwork) -> URL
var areDbFilesPresentFor: (ZcashNetwork) -> Bool
}

View File

@ -14,33 +14,30 @@ extension DatabaseFilesClient: DependencyKey {
static func live(databaseFiles: DatabaseFiles = DatabaseFiles(fileManager: .live)) -> Self {
Self(
documentsDirectory: {
try databaseFiles.documentsDirectory()
databaseFiles.documentsDirectory()
},
fsBlockDbRootFor: { network in
try databaseFiles.documentsDirectory()
databaseFiles.documentsDirectory()
.appendingPathComponent(network.networkType.chainName)
.appendingPathComponent(ZcashSDK.defaultFsCacheName, isDirectory: true)
},
cacheDbURLFor: { network in
try databaseFiles.cacheDbURL(for: network)
databaseFiles.cacheDbURL(for: network)
},
dataDbURLFor: { network in
try databaseFiles.dataDbURL(for: network)
databaseFiles.dataDbURL(for: network)
},
outputParamsURLFor: { network in
try databaseFiles.outputParamsURL(for: network)
databaseFiles.outputParamsURL(for: network)
},
pendingDbURLFor: { network in
try databaseFiles.pendingDbURL(for: network)
databaseFiles.pendingDbURL(for: network)
},
spendParamsURLFor: { network in
try databaseFiles.spendParamsURL(for: network)
databaseFiles.spendParamsURL(for: network)
},
areDbFilesPresentFor: { network in
try databaseFiles.areDbFilesPresent(for: network)
},
nukeDbFilesFor: { network in
try databaseFiles.nukeDbFiles(for: network)
databaseFiles.areDbFilesPresent(for: network)
}
)
}

View File

@ -18,8 +18,7 @@ extension DatabaseFilesClient: TestDependencyKey {
outputParamsURLFor: XCTUnimplemented("\(Self.self).outputParamsURLFor", placeholder: .emptyURL),
pendingDbURLFor: XCTUnimplemented("\(Self.self).pendingDbURLFor", placeholder: .emptyURL),
spendParamsURLFor: XCTUnimplemented("\(Self.self).spendParamsURLFor", placeholder: .emptyURL),
areDbFilesPresentFor: XCTUnimplemented("\(Self.self).areDbFilesPresentFor", placeholder: false),
nukeDbFilesFor: XCTUnimplemented("\(Self.self).nukeDbFilesFor")
areDbFilesPresentFor: XCTUnimplemented("\(Self.self).areDbFilesPresentFor", placeholder: false)
)
}
@ -38,7 +37,6 @@ extension DatabaseFilesClient {
outputParamsURLFor: { _ in .emptyURL },
pendingDbURLFor: { _ in .emptyURL },
spendParamsURLFor: { _ in .emptyURL },
areDbFilesPresentFor: { _ in false },
nukeDbFilesFor: { _ in }
areDbFilesPresentFor: { _ in false }
)
}

View File

@ -12,8 +12,8 @@ import ZcashLightClientKit
extension DependencyValues {
var sdkSynchronizer: SDKSynchronizerClient {
get { self[SDKSynchronizerDependency.self] }
set { self[SDKSynchronizerDependency.self] = newValue }
get { self[SDKSynchronizerClient.self] }
set { self[SDKSynchronizerClient.self] = newValue }
}
}
@ -25,57 +25,35 @@ enum SDKSynchronizerState: Equatable {
case unknown
}
enum SDKSynchronizerClientError: Error {
case synchronizerNotInitialized
}
protocol SDKSynchronizerClient {
var notificationCenter: NotificationCenterClient { get }
var synchronizer: SDKSynchronizer? { get }
var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never> { get }
var walletBirthday: BlockHeight? { get }
var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState? { get }
func prepareWith(initializer: Initializer, seedBytes: [UInt8], viewingKey: UnifiedFullViewingKey, walletBirthday: BlockHeight) throws
func start(retry: Bool) throws
func stop()
func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?)
func statusSnapshot() -> SyncStatusSnapshot
func isSyncing() -> Bool
func isInitialized() -> Bool
func rewind(_ policy: RewindPolicy) -> AnyPublisher<Void, Error>?
func getShieldedBalance() -> WalletBalance?
func getTransparentBalance() -> WalletBalance?
func getAllSentTransactions() -> EffectTask<[WalletEvent]>
func getAllReceivedTransactions() -> EffectTask<[WalletEvent]>
func getAllClearedTransactions() -> EffectTask<[WalletEvent]>
func getAllPendingTransactions() -> EffectTask<[WalletEvent]>
func getAllTransactions() -> EffectTask<[WalletEvent]>
func getUnifiedAddress(account: Int) -> UnifiedAddress?
func getTransparentAddress(account: Int) -> TransparentAddress?
func getSaplingAddress(accountIndex: Int) async -> SaplingAddress?
func sendTransaction(
with spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to recipientAddress: Recipient,
memo: Memo?
) -> EffectTask<Result<TransactionState, NSError>>
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,
shieldingThreshold: Zatoshi
) async throws -> TransactionState
func wipe() -> AnyPublisher<Void, Error>?
}
extension SDKSynchronizerClient {
func start() throws {
try start(retry: false)
}
struct SDKSynchronizerClient {
let stateChangedStream: () -> CurrentValueSubject<SDKSynchronizerState, Never>
let latestScannedSynchronizerState: () -> SDKSynchronizer.SynchronizerState?
let latestScannedHeight: () -> BlockHeight
let prepareWith: ([UInt8], UnifiedFullViewingKey, BlockHeight) throws -> Void
let start: (_ retry: Bool) throws -> Void
let stop: () -> Void
let statusSnapshot: () -> SyncStatusSnapshot
let isSyncing: () -> Bool
let isInitialized: () -> Bool
let rewind: (RewindPolicy) -> AnyPublisher<Void, Error>
let getShieldedBalance: () -> WalletBalance?
let getTransparentBalance: () -> WalletBalance?
let getAllSentTransactions: () -> EffectTask<[WalletEvent]>
let getAllReceivedTransactions: () -> EffectTask<[WalletEvent]>
let getAllClearedTransactions: () -> EffectTask<[WalletEvent]>
let getAllPendingTransactions: () -> EffectTask<[WalletEvent]>
let getAllTransactions: () -> EffectTask<[WalletEvent]>
let getUnifiedAddress: (_ account: Int) -> UnifiedAddress?
let getTransparentAddress: (_ account: Int) -> TransparentAddress?
let getSaplingAddress: (_ accountIndex: Int) async -> SaplingAddress?
let sendTransaction: (UnifiedSpendingKey, Zatoshi, Recipient, Memo?) -> EffectTask<Result<TransactionState, NSError>>
let shieldFunds: (UnifiedSpendingKey, Memo, Zatoshi) async throws -> TransactionState
let wipe: () -> AnyPublisher<Void, Error>?
}

View File

@ -10,33 +10,42 @@ import Combine
import ComposableArchitecture
import ZcashLightClientKit
enum SDKSynchronizerDependency: DependencyKey {
static let liveValue: SDKSynchronizerClient = LiveSDKSynchronizerClient()
}
class LiveSDKSynchronizerClient: SDKSynchronizerClient {
private class SDKSynchronizerLiveWrapper {
private var cancellables: [AnyCancellable] = []
private(set) var synchronizer: SDKSynchronizer?
// TODO: [#497] Since 0.17.0-beta SDKSynchronizer has `lastState` property which does exactly the same as `stateChanged`. Problem is that we have
// synchronizer as optional. And now it would be complicated to handle the situation when `lastState` isn't always available. Let's handle this
// in future.
private(set) var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
private(set) var notificationCenter: NotificationCenterClient
private(set) var walletBirthday: BlockHeight?
private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
let synchronizer: SDKSynchronizer
let stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
init(notificationCenter: NotificationCenterClient = .live) {
self.notificationCenter = notificationCenter
init(
notificationCenter: NotificationCenterClient = .live,
databaseFiles: DatabaseFilesClient = .liveValue,
environment: ZcashSDKEnvironment = .liveValue
) {
self.stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(.unknown)
let network = environment.network
let initializer = Initializer(
cacheDbURL: databaseFiles.cacheDbURLFor(network),
fsBlockDbRoot: databaseFiles.fsBlockDbRootFor(network),
dataDbURL: databaseFiles.dataDbURLFor(network),
pendingDbURL: databaseFiles.pendingDbURLFor(network),
endpoint: environment.endpoint,
network: network,
spendParamsURL: databaseFiles.spendParamsURLFor(network),
outputParamsURL: databaseFiles.outputParamsURLFor(network),
saplingParamsSourceURL: SaplingParamsSourceURL.default,
loggerProxy: OSLogger(logLevel: .debug, category: LoggerConstants.sdkLogs)
)
synchronizer = SDKSynchronizer(initializer: initializer)
subscribeToNotifications(notificationCenter: notificationCenter)
}
deinit {
synchronizer?.stop()
synchronizer.stop()
}
func prepareWith(initializer: Initializer, seedBytes: [UInt8], viewingKey: UnifiedFullViewingKey, walletBirthday: BlockHeight) throws {
let synchronizer = SDKSynchronizer(initializer: initializer)
private func subscribeToNotifications(notificationCenter: NotificationCenterClient) {
notificationCenter.publisherFor(.synchronizerStarted)?
.receive(on: DispatchQueue.main)
.sink { [weak self] notif in
@ -62,197 +71,160 @@ class LiveSDKSynchronizerClient: SDKSynchronizerClient {
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.synchronizerStopped() }
.store(in: &cancellables)
guard try synchronizer.prepare(with: seedBytes, viewingKeys: [viewingKey], walletBirthday: walletBirthday) == .success else {
throw SynchronizerError.initFailed(message: "")
}
self.synchronizer = synchronizer
self.walletBirthday = initializer.walletBirthday
}
func start(retry: Bool) throws {
try synchronizer?.start(retry: retry)
}
func stop() {
synchronizer?.stop()
}
func isSyncing() -> Bool {
latestScannedSynchronizerState?.syncStatus.isSyncing ?? false
}
func isInitialized() -> Bool {
synchronizer != nil
}
func synchronizerStarted(_ synchronizerState: SDKSynchronizer.SynchronizerState?) {
private func synchronizerStarted(_ synchronizerState: SDKSynchronizer.SynchronizerState?) {
latestScannedSynchronizerState = synchronizerState
stateChanged.send(.started)
}
func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) {
private func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) {
latestScannedSynchronizerState = synchronizerState
stateChanged.send(.synced)
}
func synchronizerProgressUpdated() {
private func synchronizerProgressUpdated() {
stateChanged.send(.progressUpdated)
}
func synchronizerStopped() {
private func synchronizerStopped() {
stateChanged.send(.stopped)
}
}
func statusSnapshot() -> SyncStatusSnapshot {
guard let synchronizer else {
return .default
}
return SyncStatusSnapshot.snapshotFor(state: synchronizer.status)
}
extension SDKSynchronizerClient: DependencyKey {
static let liveValue: SDKSynchronizerClient = Self.live()
func rewind(_ policy: RewindPolicy) -> AnyPublisher<Void, Error>? {
return synchronizer?.rewind(policy)
}
func getShieldedBalance() -> WalletBalance? {
latestScannedSynchronizerState?.shieldedBalance
}
static func live(
notificationCenter: NotificationCenterClient = .live,
databaseFiles: DatabaseFilesClient = .liveValue,
environment: ZcashSDKEnvironment = .liveValue
) -> Self {
let sdkSynchronizerWrapper = SDKSynchronizerLiveWrapper(
notificationCenter: notificationCenter,
databaseFiles: databaseFiles,
environment: environment
)
let synchronizer = sdkSynchronizerWrapper.synchronizer
func getTransparentBalance() -> WalletBalance? {
latestScannedSynchronizerState?.transparentBalance
}
return SDKSynchronizerClient(
stateChangedStream: { sdkSynchronizerWrapper.stateChanged },
latestScannedSynchronizerState: { sdkSynchronizerWrapper.latestScannedSynchronizerState },
latestScannedHeight: { synchronizer.latestScannedHeight },
prepareWith: { seedBytes, viewingKey, walletBirtday in
let result = try synchronizer.prepare(with: seedBytes, viewingKeys: [viewingKey], walletBirthday: walletBirtday)
func getAllSentTransactions() -> EffectTask<[WalletEvent]> {
if let transactions = try? synchronizer?.allSentTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer?.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
}
return .none
}
func getAllReceivedTransactions() -> EffectTask<[WalletEvent]> {
if let transactions = try? synchronizer?.allReceivedTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer?.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
}
return .none
}
func getAllClearedTransactions() -> EffectTask<[WalletEvent]> {
if let transactions = try? synchronizer?.allClearedTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer?.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
}
return .none
}
func getAllPendingTransactions() -> EffectTask<[WalletEvent]> {
if let transactions = try? synchronizer?.allPendingTransactions(),
let syncedBlockHeight = synchronizer?.latestScannedHeight {
return EffectTask(value: transactions.map {
let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: syncedBlockHeight)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
})
}
return .none
}
func getAllTransactions() -> EffectTask<[WalletEvent]> {
if let pendingTransactions = try? synchronizer?.allPendingTransactions(),
let clearedTransactions = try? synchronizer?.allClearedTransactions(),
let syncedBlockHeight = synchronizer?.latestScannedHeight {
let clearedTxs: [WalletEvent] = clearedTransactions.map {
let transaction = TransactionState.init(transaction: $0)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
let pendingTxs: [WalletEvent] = pendingTransactions.map {
let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: syncedBlockHeight)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
let cTxs = clearedTxs.filter { transaction in
pendingTxs.first { pending in
pending.id == transaction.id
} == nil
}
return .merge(
EffectTask(value: cTxs),
EffectTask(value: pendingTxs)
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
}
return .none
}
func getUnifiedAddress(account: Int) -> UnifiedAddress? {
synchronizer?.getUnifiedAddress(accountIndex: account)
}
func getTransparentAddress(account: Int) -> TransparentAddress? {
synchronizer?.getTransparentAddress(accountIndex: account)
}
func getSaplingAddress(accountIndex: Int) async -> SaplingAddress? {
await synchronizer?.getSaplingAddress(accountIndex: accountIndex)
}
func sendTransaction(
with spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to recipientAddress: Recipient,
memo: Memo?
) -> EffectTask<Result<TransactionState, NSError>> {
return .run { [weak self] send in
do {
guard let synchronizer = self?.synchronizer else {
await send(.failure(SDKSynchronizerClientError.synchronizerNotInitialized as NSError))
return
if result != .success {
throw SynchronizerError.initFailed(message: "")
}
},
start: { retry in try synchronizer.start(retry: retry) },
stop: { synchronizer.stop() },
statusSnapshot: { SyncStatusSnapshot.snapshotFor(state: synchronizer.status) },
isSyncing: { sdkSynchronizerWrapper.latestScannedSynchronizerState?.syncStatus.isSyncing ?? false },
isInitialized: { synchronizer.status != .unprepared },
rewind: { synchronizer.rewind($0) },
getShieldedBalance: { sdkSynchronizerWrapper.latestScannedSynchronizerState?.shieldedBalance },
getTransparentBalance: { sdkSynchronizerWrapper.latestScannedSynchronizerState?.transparentBalance },
getAllSentTransactions: {
if let transactions = try? synchronizer.allSentTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
}
let pendingTransaction = try await synchronizer.sendToAddress(
return .none
},
getAllReceivedTransactions: {
if let transactions = try? synchronizer.allReceivedTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
}
return .none
},
getAllClearedTransactions: {
if let transactions = try? synchronizer.allClearedTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
}
return .none
},
getAllPendingTransactions: {
if let transactions = try? synchronizer.allPendingTransactions() {
return EffectTask(value: transactions.map {
let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: synchronizer.latestScannedHeight)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
})
}
return .none
},
getAllTransactions: {
if let pendingTransactions = try? synchronizer.allPendingTransactions(),
let clearedTransactions = try? synchronizer.allClearedTransactions() {
let clearedTxs: [WalletEvent] = clearedTransactions.map {
let transaction = TransactionState.init(transaction: $0)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
let pendingTxs: [WalletEvent] = pendingTransactions.map {
let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: synchronizer.latestScannedHeight)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
let cTxs = clearedTxs.filter { transaction in
pendingTxs.first { pending in
pending.id == transaction.id
} == nil
}
return .merge(
EffectTask(value: cTxs),
EffectTask(value: pendingTxs)
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
}
return .none
},
getUnifiedAddress: { synchronizer.getUnifiedAddress(accountIndex: $0) },
getTransparentAddress: { synchronizer.getTransparentAddress(accountIndex: $0) },
getSaplingAddress: { await synchronizer.getSaplingAddress(accountIndex: $0) },
sendTransaction: { spendingKey, amount, recipient, memo in
return .run { send in
do {
let pendingTransaction = try await synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: amount,
toAddress: recipient,
memo: memo
)
await send(.success(TransactionState(pendingTransaction: pendingTransaction)))
} catch {
await send(.failure(error as NSError))
}
}
},
shieldFunds: { spendingKey, memo, shieldingThreshold in
let pendingTransaction = try await synchronizer.shieldFunds(
spendingKey: spendingKey,
zatoshi: zatoshi,
toAddress: recipientAddress,
memo: memo
memo: memo,
shieldingThreshold: shieldingThreshold
)
await send(.success(TransactionState(pendingTransaction: pendingTransaction)))
} catch {
await send(.failure(error as NSError))
}
}
}
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,
shieldingThreshold: Zatoshi
) async throws -> TransactionState {
guard let synchronizer = synchronizer else { throw SDKSynchronizerClientError.synchronizerNotInitialized }
let pendingTransaction = try await synchronizer.shieldFunds(spendingKey: spendingKey, memo: memo, shieldingThreshold: shieldingThreshold)
return TransactionState(pendingTransaction: pendingTransaction)
}
func wipe() -> AnyPublisher<Void, Error>? {
synchronizer?.wipe()
return TransactionState(pendingTransaction: pendingTransaction)
},
wipe: { synchronizer.wipe() }
)
}
}

View File

@ -1,240 +0,0 @@
//
// SDKSynchronizerMocks.swift
// secant-testnet
//
// Created by Lukáš Korba on 15.11.2022.
//
import Foundation
import Combine
import ComposableArchitecture
import ZcashLightClientKit
extension SDKSynchronizerDependency {
static let mock: SDKSynchronizerClient = MockSDKSynchronizerClient()
static func mockWithSnapshot(_ snapshot: SyncStatusSnapshot) -> MockSDKSynchronizerClient {
MockSDKSynchronizerClient(snapshot: snapshot)
}
}
class MockSDKSynchronizerClient: SDKSynchronizerClient {
private var cancellables: [AnyCancellable] = []
private var snapshot: SyncStatusSnapshot
private(set) var notificationCenter: NotificationCenterClient
private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
private(set) var walletBirthday: BlockHeight?
private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
init(
notificationCenter: NotificationCenterClient = .noOp,
snapshot: SyncStatusSnapshot = .default
) {
self.notificationCenter = notificationCenter
self.stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(.unknown)
self.snapshot = snapshot
}
func prepareWith(initializer: Initializer, seedBytes: [UInt8], viewingKey: UnifiedFullViewingKey, walletBirthday: BlockHeight) throws { }
func start(retry: Bool) throws { }
func stop() { }
func isSyncing() -> Bool { false }
func isInitialized() -> Bool { false }
func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { }
func statusSnapshot() -> SyncStatusSnapshot { self.snapshot }
func rewind(_ policy: RewindPolicy) -> AnyPublisher<Void, Error>? { Empty<Void, Error>().eraseToAnyPublisher() }
func getShieldedBalance() -> WalletBalance? {
WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000))
}
func getTransparentBalance() -> WalletBalance? {
WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000))
}
func getAllSentTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
}
func getAllReceivedTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
}
func getAllClearedTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
}
func getAllPendingTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(
date: 1651039606,
amount: Zatoshi(6),
status: .paid(success: false),
uuid: "ff66"
),
TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"),
TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"),
TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
)
}
func getAllTransactions() -> EffectTask<[WalletEvent]> {
return .merge(
getAllClearedTransactions(),
getAllPendingTransactions()
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
}
func getUnifiedAddress(account: Int) -> UnifiedAddress? {
// swiftlint:disable force_try
try! UnifiedAddress(
encoding: """
utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyz\
wtgnuc76h
""",
network: .testnet
)
}
func getTransparentAddress(account: Int) -> TransparentAddress? { nil }
func getSaplingAddress(accountIndex account: Int) -> SaplingAddress? {
// swiftlint:disable:next force_try
try! SaplingAddress(encoding: "ztestsapling1edm52k336nk70gxqxedd89slrrf5xwnnp5rt6gqnk0tgw4mynv6fcx42ym6x27yac5amvfvwypz", network: .testnet)
}
func sendTransaction(
with spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to recipientAddress: Recipient,
memo: Memo?
) -> EffectTask<Result<TransactionState, NSError>> {
var memos: [Memo]? = []
if let memo { memos?.append(memo) }
let transactionState = TransactionState(
expiryHeight: 40,
memos: memos,
minedHeight: 50,
shielded: true,
zAddress: "tteafadlamnelkqe",
fee: Zatoshi(10),
id: "id",
status: .paid(success: true),
timestamp: 1234567,
zecAmount: Zatoshi(10)
)
return EffectTask(value: Result.success(transactionState))
}
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,
shieldingThreshold: Zatoshi
) async throws -> TransactionState {
return TransactionState(
expiryHeight: 40,
memos: [memo],
minedHeight: 50,
shielded: true,
zAddress: "tteafadlamnelkqe",
fee: Zatoshi(10),
id: "id",
status: .paid(success: true),
timestamp: 1234567,
zecAmount: Zatoshi(10)
)
}
func wipe() -> AnyPublisher<Void, Error>? { nil }
}

View File

@ -10,257 +10,317 @@ import ComposableArchitecture
import Foundation
import ZcashLightClientKit
extension SDKSynchronizerDependency: TestDependencyKey {
static let testValue: SDKSynchronizerClient = NoopSDKSynchronizer()
extension SDKSynchronizerClient: TestDependencyKey {
static let testValue = Self(
stateChangedStream: XCTUnimplemented("\(Self.self).stateChangedStream", placeholder: CurrentValueSubject(SDKSynchronizerState.unknown)),
latestScannedSynchronizerState: XCTUnimplemented("\(Self.self).latestScannedSynchronizerState", placeholder: .zero),
latestScannedHeight: XCTUnimplemented("\(Self.self).latestScannedHeight", placeholder: 0),
prepareWith: XCTUnimplemented("\(Self.self).prepareWith"),
start: XCTUnimplemented("\(Self.self).start"),
stop: XCTUnimplemented("\(Self.self).stop"),
statusSnapshot: XCTUnimplemented("\(Self.self).statusSnapshot", placeholder: SyncStatusSnapshot.snapshotFor(state: .unprepared)),
isSyncing: XCTUnimplemented("\(Self.self).isSyncing", placeholder: false),
isInitialized: XCTUnimplemented("\(Self.self).isInitialized", placeholder: false),
rewind: XCTUnimplemented("\(Self.self).rewind", placeholder: Fail(error: "Error").eraseToAnyPublisher()),
getShieldedBalance: XCTUnimplemented("\(Self.self).getShieldedBalance", placeholder: WalletBalance.zero),
getTransparentBalance: XCTUnimplemented("\(Self.self).getTransparentBalance", placeholder: WalletBalance.zero),
getAllSentTransactions: XCTUnimplemented("\(Self.self).getAllSentTransactions", placeholder: .none),
getAllReceivedTransactions: XCTUnimplemented("\(Self.self).getAllReceivedTransactions", placeholder: .none),
getAllClearedTransactions: XCTUnimplemented("\(Self.self).getAllClearedTransactions", placeholder: .none),
getAllPendingTransactions: XCTUnimplemented("\(Self.self).getAllPendingTransactions", placeholder: .none),
getAllTransactions: XCTUnimplemented("\(Self.self).getAllTransactions", placeholder: .none),
getUnifiedAddress: XCTUnimplemented("\(Self.self).getUnifiedAddress", placeholder: nil),
getTransparentAddress: XCTUnimplemented("\(Self.self).getTransparentAddress", placeholder: nil),
getSaplingAddress: XCTUnimplemented("\(Self.self).getSaplingAddress", placeholder: nil),
sendTransaction: XCTUnimplemented("\(Self.self).sendTransaction", placeholder: .none),
shieldFunds: XCTUnimplemented("\(Self.self).shieldFunds", placeholder: .placeholder),
wipe: XCTUnimplemented("\(Self.self).wipe")
)
}
class NoopSDKSynchronizer: SDKSynchronizerClient {
private(set) var notificationCenter: NotificationCenterClient
private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
private(set) var walletBirthday: BlockHeight?
private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
extension SDKSynchronizerClient {
static let noOp = Self(
stateChangedStream: { CurrentValueSubject<SDKSynchronizerState, Never>(.unknown) },
latestScannedSynchronizerState: { nil },
latestScannedHeight: { 0 },
prepareWith: { _, _, _ in },
start: { _ in },
stop: { },
statusSnapshot: { SyncStatusSnapshot.snapshotFor(state: .unprepared) },
isSyncing: { false },
isInitialized: { false },
rewind: { _ in return Empty<Void, Error>().eraseToAnyPublisher() },
getShieldedBalance: { .zero },
getTransparentBalance: { .zero },
getAllSentTransactions: { EffectTask(value: []) },
getAllReceivedTransactions: { EffectTask(value: []) },
getAllClearedTransactions: { EffectTask(value: []) },
getAllPendingTransactions: { EffectTask(value: []) },
getAllTransactions: { EffectTask(value: []) },
getUnifiedAddress: { _ in return nil },
getTransparentAddress: { _ in return nil },
getSaplingAddress: { _ in return nil },
sendTransaction: { _, _, _, _ in return EffectTask(value: Result.failure(SynchronizerError.criticalError as NSError)) },
shieldFunds: { _, _, _ in return .placeholder },
wipe: { Empty<Void, Error>().eraseToAnyPublisher() }
)
init(notificationCenter: NotificationCenterClient = .noOp) {
self.notificationCenter = notificationCenter
self.stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(.unknown)
}
func prepareWith(initializer: Initializer, seedBytes: [UInt8], viewingKey: UnifiedFullViewingKey, walletBirthday: BlockHeight) throws { }
func start(retry: Bool) throws { }
func stop() { }
func isSyncing() -> Bool { false }
func isInitialized() -> Bool { false }
func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { }
func statusSnapshot() -> SyncStatusSnapshot { .default }
func rewind(_ policy: RewindPolicy) -> AnyPublisher<Void, Error>? { Empty<Void, Error>().eraseToAnyPublisher() }
func getShieldedBalance() -> WalletBalance? { nil }
func getTransparentBalance() -> WalletBalance? { nil }
func getAllSentTransactions() -> EffectTask<[WalletEvent]> { EffectTask(value: []) }
func getAllReceivedTransactions() -> EffectTask<[WalletEvent]> { EffectTask(value: []) }
func getAllClearedTransactions() -> EffectTask<[WalletEvent]> { EffectTask(value: []) }
func getAllPendingTransactions() -> EffectTask<[WalletEvent]> { EffectTask(value: []) }
func getAllTransactions() -> EffectTask<[WalletEvent]> { EffectTask(value: []) }
func getUnifiedAddress(account: Int) -> UnifiedAddress? { nil }
func getTransparentAddress(account: Int) -> TransparentAddress? { nil }
func getSaplingAddress(accountIndex account: Int) -> SaplingAddress? { nil }
func sendTransaction(
with spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to recipientAddress: Recipient,
memo: Memo?
) -> EffectTask<Result<TransactionState, NSError>> {
EffectTask(value: Result.failure(SynchronizerError.criticalError as NSError))
}
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,
shieldingThreshold: Zatoshi
) async throws -> TransactionState {
throw SynchronizerError.criticalError
}
func updateStateChanged(_ newState: SDKSynchronizerState) {
stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(newState)
}
func wipe() -> AnyPublisher<Void, Error>? { nil }
static let mock = Self.mocked()
}
class TestSDKSynchronizerClient: SDKSynchronizerClient {
private(set) var notificationCenter: NotificationCenterClient
private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
private(set) var walletBirthday: BlockHeight?
private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
extension SDKSynchronizerClient {
static func mocked(
stateChangedStream: @escaping () -> CurrentValueSubject<SDKSynchronizerState, Never> = {
CurrentValueSubject<SDKSynchronizerState, Never>(.synced)
},
latestScannedSynchronizerState: @escaping () -> SDKSynchronizer.SynchronizerState? = { nil },
latestScannedHeight: @escaping () -> BlockHeight = { 0 },
prepareWith: @escaping ([UInt8], UnifiedFullViewingKey, BlockHeight) throws -> Void = { _, _, _ in },
start: @escaping (_ retry: Bool) throws -> Void = { _ in },
stop: @escaping () -> Void = { },
statusSnapshot: @escaping () -> SyncStatusSnapshot = { SyncStatusSnapshot.snapshotFor(state: .unprepared) },
isSyncing: @escaping () -> Bool = { false },
isInitialized: @escaping () -> Bool = { false },
rewind: @escaping (RewindPolicy) -> AnyPublisher<Void, Error> = { _ in return Empty<Void, Error>().eraseToAnyPublisher() },
getShieldedBalance: @escaping () -> WalletBalance? = { WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) },
getTransparentBalance: @escaping () -> WalletBalance? = { WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) },
getAllSentTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
init(notificationCenter: NotificationCenterClient = .noOp) {
self.notificationCenter = notificationCenter
self.stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(.unknown)
}
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
},
getAllReceivedTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
func prepareWith(initializer: Initializer, seedBytes: [UInt8], viewingKey: UnifiedFullViewingKey, walletBirthday: BlockHeight) throws { }
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
},
getAllClearedTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
func start(retry: Bool) throws { }
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
},
getAllPendingTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(
date: 1651039606,
amount: Zatoshi(6),
status: .paid(success: false),
uuid: "ff66"
),
TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"),
TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"),
TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99")
]
func stop() { }
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
)
},
getAllTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
let mockerCleared: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
func isSyncing() -> Bool { false }
let clearedTransactionsEffect = EffectTask(
value:
mockerCleared.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
func isInitialized() -> Bool { false }
let mockedPending: [TransactionStateMockHelper] = [
TransactionStateMockHelper(
date: 1651039606,
amount: Zatoshi(6),
status: .paid(success: false),
uuid: "ff66"
),
TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"),
TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"),
TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99")
]
func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { }
let pendingTransactionsEffect = EffectTask(
value:
mockedPending.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
)
func statusSnapshot() -> SyncStatusSnapshot { .default }
return .merge(
clearedTransactionsEffect,
pendingTransactionsEffect
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
},
getUnifiedAddress: @escaping (_ account: Int) -> UnifiedAddress? = { _ in
// swiftlint:disable force_try
try! UnifiedAddress(
encoding: """
utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7d\
wyzwtgnuc76h
""",
network: .testnet
)
},
getTransparentAddress: @escaping (_ account: Int) -> TransparentAddress? = { _ in return nil },
getSaplingAddress: @escaping (_ accountIndex: Int) async -> SaplingAddress? = { _ in
// swiftlint:disable:next force_try
try! SaplingAddress(
encoding: "ztestsapling1edm52k336nk70gxqxedd89slrrf5xwnnp5rt6gqnk0tgw4mynv6fcx42ym6x27yac5amvfvwypz",
network: .testnet
)
},
sendTransaction:
@escaping (UnifiedSpendingKey, Zatoshi, Recipient, Memo?) -> EffectTask<Result<TransactionState, NSError>> = { _, _, _, memo in
var memos: [Memo]? = []
if let memo { memos?.append(memo) }
func rewind(_ policy: RewindPolicy) -> AnyPublisher<Void, Error>? { nil }
func getShieldedBalance() -> WalletBalance? { nil }
func getTransparentBalance() -> WalletBalance? { nil }
let transactionState = TransactionState(
expiryHeight: 40,
memos: memos,
minedHeight: 50,
shielded: true,
zAddress: "tteafadlamnelkqe",
fee: Zatoshi(10),
id: "id",
status: .paid(success: true),
timestamp: 1234567,
zecAmount: Zatoshi(10)
)
func getAllSentTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
return EffectTask(value: Result.success(transactionState))
},
shieldFunds: @escaping (UnifiedSpendingKey, Memo, Zatoshi) async throws -> TransactionState = { _, memo, _ in
return TransactionState(
expiryHeight: 40,
memos: [memo],
minedHeight: 50,
shielded: true,
zAddress: "tteafadlamnelkqe",
fee: Zatoshi(10),
id: "id",
status: .paid(success: true),
timestamp: 1234567,
zecAmount: Zatoshi(10)
)
},
wipe: @escaping () -> AnyPublisher<Void, Error>? = { Fail(error: "Error").eraseToAnyPublisher() }
) -> SDKSynchronizerClient {
SDKSynchronizerClient(
stateChangedStream: stateChangedStream,
latestScannedSynchronizerState: latestScannedSynchronizerState,
latestScannedHeight: latestScannedHeight,
prepareWith: prepareWith,
start: start,
stop: stop,
statusSnapshot: statusSnapshot,
isSyncing: isSyncing,
isInitialized: isInitialized,
rewind: rewind,
getShieldedBalance: getShieldedBalance,
getTransparentBalance: getTransparentBalance,
getAllSentTransactions: getAllSentTransactions,
getAllReceivedTransactions: getAllReceivedTransactions,
getAllClearedTransactions: getAllClearedTransactions,
getAllPendingTransactions: getAllPendingTransactions,
getAllTransactions: getAllTransactions,
getUnifiedAddress: getUnifiedAddress,
getTransparentAddress: getTransparentAddress,
getSaplingAddress: getSaplingAddress,
sendTransaction: sendTransaction,
shieldFunds: shieldFunds,
wipe: wipe
)
}
func getAllReceivedTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
)
}
func getAllClearedTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"),
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
)
}
func getAllPendingTransactions() -> EffectTask<[WalletEvent]> {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(
date: 1651039606,
amount: Zatoshi(6),
status: .paid(success: false),
uuid: "ff66"
),
TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"),
TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"),
TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
)
}
func getAllTransactions() -> EffectTask<[WalletEvent]> {
return .merge(
getAllClearedTransactions(),
getAllPendingTransactions()
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
}
func getUnifiedAddress(account: Int) -> UnifiedAddress? { nil }
func getTransparentAddress(account: Int) -> TransparentAddress? { nil }
func getSaplingAddress(accountIndex account: Int) -> SaplingAddress? {
// swiftlint:disable:next force_try
try! SaplingAddress(encoding: "ztestsapling1edm52k336nk70gxqxedd89slrrf5xwnnp5rt6gqnk0tgw4mynv6fcx42ym6x27yac5amvfvwypz", network: .testnet)
}
func sendTransaction(
with spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to recipientAddress: Recipient,
memo: Memo?
) -> EffectTask<Result<TransactionState, NSError>> {
return EffectTask(value: Result.failure(SynchronizerError.criticalError as NSError))
}
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,
shieldingThreshold: Zatoshi
) async throws -> TransactionState {
throw SynchronizerError.criticalError
}
func updateStateChanged(_ newState: SDKSynchronizerState) {
stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(newState)
}
func wipe() -> AnyPublisher<Void, Error>? { nil }
}

View File

@ -68,7 +68,7 @@ struct BalanceBreakdownReducer: ReducerProtocol {
return .none
case .onAppear:
return sdkSynchronizer.stateChanged
return sdkSynchronizer.stateChangedStream()
.map(BalanceBreakdownReducer.Action.synchronizerStateChanged)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
@ -84,11 +84,7 @@ struct BalanceBreakdownReducer: ReducerProtocol {
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0)
_ = try await sdkSynchronizer.shieldFunds(
spendingKey: spendingKey,
memo: Memo(string: ""),
shieldingThreshold: state.autoShieldingThreshold
)
_ = try await sdkSynchronizer.shieldFunds(spendingKey, Memo(string: ""), state.autoShieldingThreshold)
await send(.shieldFundsSuccess)
} catch {
@ -121,17 +117,17 @@ struct BalanceBreakdownReducer: ReducerProtocol {
return EffectTask(value: .updateSynchronizerStatus)
case .updateSynchronizerStatus:
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState?.shieldedBalance {
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState()?.shieldedBalance {
state.shieldedBalance = shieldedBalance.redacted
}
if let transparentBalance = sdkSynchronizer.latestScannedSynchronizerState?.transparentBalance {
if let transparentBalance = sdkSynchronizer.latestScannedSynchronizerState()?.transparentBalance {
state.transparentBalance = transparentBalance.redacted
}
return EffectTask(value: .updateLatestBlock)
case .updateLatestBlock:
guard
let latestBlockNumber = sdkSynchronizer.latestScannedSynchronizerState?.latestScannedHeight,
let latestBlockNumber = sdkSynchronizer.latestScannedSynchronizerState()?.latestScannedHeight,
let latestBlock = numberFormatter.string(NSDecimalNumber(value: latestBlockNumber))
else {
state.latestBlock = L10n.General.unknown

View File

@ -112,7 +112,7 @@ struct HomeReducer: ReducerProtocol {
state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations
if diskSpaceChecker.hasEnoughFreeSpaceForSync() {
let syncEffect = sdkSynchronizer.stateChanged
let syncEffect = sdkSynchronizer.stateChangedStream()
.map(HomeReducer.Action.synchronizerStateChanged)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
@ -138,7 +138,7 @@ struct HomeReducer: ReducerProtocol {
}
state.synchronizerStatusSnapshot = snapshot
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState?.shieldedBalance {
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState()?.shieldedBalance {
state.shieldedBalance = shieldedBalance.redacted
}
@ -179,7 +179,7 @@ struct HomeReducer: ReducerProtocol {
case .retrySync:
do {
try sdkSynchronizer.start(retry: true)
try sdkSynchronizer.start(true)
} catch {
state.alert = AlertState<HomeReducer.Action>(
title: TextState(L10n.Home.SyncFailed.title),

View File

@ -43,7 +43,7 @@ struct ProfileReducer: ReducerProtocol {
Reduce { state, action in
switch action {
case .onAppear:
state.addressDetailsState.uAddress = self.sdkSynchronizer.getUnifiedAddress(account: 0)
state.addressDetailsState.uAddress = self.sdkSynchronizer.getUnifiedAddress(0)
state.appBuild = appVersion.appBuild()
state.appVersion = appVersion.appVersion()
state.sdkVersion = zcashSDKEnvironment.sdkVersion

View File

@ -71,7 +71,7 @@ extension RootReducer {
)
} else {
do {
try sdkSynchronizer.start()
try sdkSynchronizer.start(false)
} catch {
// TODO: [#221] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
@ -107,18 +107,7 @@ extension RootReducer {
}
private func rewind(policy: RewindPolicy, sourceAction: DebugAction) -> EffectPublisher<RootReducer.Action, Never> {
guard let rewindPublisher = sdkSynchronizer.rewind(policy) else {
return EffectTask(
value: .debug(
.rewindDone(
L10n.Root.Debug.Error.Rewind.sdkSynchronizerNotInitialized,
.debug(sourceAction)
)
)
)
}
return rewindPublisher
return sdkSynchronizer.rewind(policy)
.replaceEmpty(with: Void())
.map { _ in return RootReducer.Action.debug(.rewindDone(nil, .debug(sourceAction))) }
.catch { error in

View File

@ -77,7 +77,7 @@ extension RootReducer {
case .destination(.deeplink(let url)):
// get the latest synchronizer state
let synchronizerStatus = sdkSynchronizer.stateChanged.value
let synchronizerStatus = sdkSynchronizer.stateChangedStream().value
// process the deeplink only if app is initialized and synchronizer synced
guard state.appInitializationState == .initialized && synchronizerStatus == .synced else {

View File

@ -130,23 +130,15 @@ extension RootReducer {
return .none
}
try mnemonic.isValid(storedWallet.seedPhrase.value())
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
let birthday = state.storedWallet?.birthday?.value() ?? zcashSDKEnvironment.latestCheckpoint
let initializer = try RootReducer.prepareInitializer(
databaseFiles: databaseFiles,
derivationTool: derivationTool,
mnemonic: mnemonic,
zcashSDKEnvironment: zcashSDKEnvironment
)
try mnemonic.isValid(storedWallet.seedPhrase.value())
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0)
let viewingKey = try spendingKey.deriveFullViewingKey()
try sdkSynchronizer.prepareWith(initializer: initializer, seedBytes: seedBytes, viewingKey: viewingKey, walletBirthday: birthday)
try sdkSynchronizer.start()
try sdkSynchronizer.prepareWith(seedBytes, viewingKey, birthday)
try sdkSynchronizer.start(false)
return .none
} catch {
state.appInitializationState = .failed

View File

@ -105,7 +105,7 @@ extension RootReducer {
var keysPresent = false
do {
keysPresent = try walletStorage.areKeysPresent()
let databaseFilesPresent = try databaseFiles.areDbFilesPresentFor(
let databaseFilesPresent = databaseFiles.areDbFilesPresentFor(
zcashSDKEnvironment.network
)
@ -119,19 +119,9 @@ extension RootReducer {
case (true, true):
return .initialized
}
} catch DatabaseFiles.DatabaseFilesError.filesPresentCheck {
if keysPresent {
return .filesMissing
}
} catch WalletStorage.WalletStorageError.uninitializedWallet {
do {
if try databaseFiles.areDbFilesPresentFor(
zcashSDKEnvironment.network
) {
return .keysMissing
}
} catch {
return .uninitialized
if databaseFiles.areDbFilesPresentFor(zcashSDKEnvironment.network) {
return .keysMissing
}
} catch {
return .failed
@ -139,34 +129,6 @@ extension RootReducer {
return .uninitialized
}
static func prepareInitializer(
databaseFiles: DatabaseFilesClient,
derivationTool: DerivationToolClient,
mnemonic: MnemonicClient,
zcashSDKEnvironment: ZcashSDKEnvironment
) throws -> Initializer {
do {
let network = zcashSDKEnvironment.network
let initializer = Initializer(
cacheDbURL: try databaseFiles.cacheDbURLFor(network),
fsBlockDbRoot: try databaseFiles.fsBlockDbRootFor(network),
dataDbURL: try databaseFiles.dataDbURLFor(network),
pendingDbURL: try databaseFiles.pendingDbURLFor(network),
endpoint: zcashSDKEnvironment.endpoint,
network: zcashSDKEnvironment.network,
spendParamsURL: try databaseFiles.spendParamsURLFor(network),
outputParamsURL: try databaseFiles.outputParamsURLFor(network),
saplingParamsSourceURL: SaplingParamsSourceURL.default,
loggerProxy: OSLogger(logLevel: .debug, category: LoggerConstants.sdkLogs)
)
return initializer
} catch {
throw SDKInitializationError.failed
}
}
}
// MARK: Placeholders

View File

@ -162,15 +162,10 @@ struct SendFlowReducer: ReducerProtocol {
}
let recipient = try Recipient(state.address, network: zcashSDKEnvironment.network.networkType)
let sendTransActionEffect = sdkSynchronizer.sendTransaction(
with: spendingKey,
zatoshi: state.amount,
to: recipient,
memo: memo
)
.receive(on: mainQueue)
.map(SendFlowReducer.Action.sendTransactionResult)
.eraseToEffect()
let sendTransActionEffect = sdkSynchronizer.sendTransaction(spendingKey, state.amount, recipient, memo)
.receive(on: mainQueue)
.map(SendFlowReducer.Action.sendTransactionResult)
.eraseToEffect()
return .concatenate(
EffectTask(value: .updateDestination(.inProgress)),
@ -200,7 +195,7 @@ struct SendFlowReducer: ReducerProtocol {
case .onAppear:
state.memoState.charLimit = zcashSDKEnvironment.memoCharLimit
return sdkSynchronizer.stateChanged
return sdkSynchronizer.stateChangedStream()
.map(SendFlowReducer.Action.synchronizerStateChanged)
.eraseToEffect()
.cancellable(id: SyncStatusUpdatesID.self, cancelInFlight: true)
@ -209,7 +204,7 @@ struct SendFlowReducer: ReducerProtocol {
return .cancel(id: SyncStatusUpdatesID.self)
case .synchronizerStateChanged(.synced):
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState?.shieldedBalance {
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState()?.shieldedBalance {
state.shieldedBalance = shieldedBalance.redacted
state.transactionAmountInputState.maxValue = shieldedBalance.total.amount.redacted
}

View File

@ -48,7 +48,7 @@ struct WalletEventsFlowReducer: ReducerProtocol {
switch action {
case .onAppear:
state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations
return sdkSynchronizer.stateChanged
return sdkSynchronizer.stateChangedStream()
.map(WalletEventsFlowReducer.Action.synchronizerStateChanged)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
@ -57,9 +57,7 @@ struct WalletEventsFlowReducer: ReducerProtocol {
return .cancel(id: CancelId.self)
case .synchronizerStateChanged(.synced):
if let latestMinedHeight = sdkSynchronizer.synchronizer?.latestScannedHeight {
state.latestMinedHeight = latestMinedHeight
}
state.latestMinedHeight = sdkSynchronizer.latestScannedHeight()
return sdkSynchronizer.getAllTransactions()
.receive(on: mainQueue)
.map(WalletEventsFlowReducer.Action.updateWalletEvents)

View File

@ -352,12 +352,6 @@ internal enum L10n {
}
}
}
internal enum Error {
internal enum Rewind {
/// SDKSynchronizer not initilized. rewindPublisher is nil
internal static let sdkSynchronizerNotInitialized = L10n.tr("Localizable", "root.debug.error.rewind.sdkSynchronizerNotInitialized", fallback: "SDKSynchronizer not initilized. rewindPublisher is nil")
}
}
internal enum Option {
/// Export logs
internal static let exportLogs = L10n.tr("Localizable", "root.debug.option.exportLogs", fallback: "Export logs")

View File

@ -251,7 +251,6 @@
"root.debug.alert.rewind.failed.message" = "Error: %@";
"root.debug.alert.rewind.cantStartSync.title" = "Can't start sync process after rewind";
"root.debug.alert.rewind.cantStartSync.message" = "Error: %@";
"root.debug.error.rewind.sdkSynchronizerNotInitialized" = "SDKSynchronizer not initilized. rewindPublisher is nil";
// MARK: - Export logs
"exportLogs.alert.failed.title" = "Error when exporting logs";

View File

@ -16,6 +16,8 @@ class BalanceBreakdownTests: XCTestCase {
initialState: .placeholder,
reducer: BalanceBreakdownReducer()
)
store.dependencies.sdkSynchronizer = .noOp
store.send(.onAppear)
@ -35,7 +37,7 @@ class BalanceBreakdownTests: XCTestCase {
reducer: BalanceBreakdownReducer()
)
store.dependencies.sdkSynchronizer = MockSDKSynchronizerClient()
store.dependencies.sdkSynchronizer = .mock
store.dependencies.derivationTool = .liveValue
store.dependencies.mnemonic = .mock
store.dependencies.walletStorage.exportWallet = { .placeholder }
@ -64,7 +66,7 @@ class BalanceBreakdownTests: XCTestCase {
reducer: BalanceBreakdownReducer()
)
store.dependencies.sdkSynchronizer = NoopSDKSynchronizer()
store.dependencies.sdkSynchronizer = .mocked(shieldFunds: { _, _, _ in throw SynchronizerError.criticalError })
store.dependencies.derivationTool = .liveValue
store.dependencies.mnemonic = .mock
store.dependencies.walletStorage.exportWallet = { .placeholder }

View File

@ -5,6 +5,7 @@
// Created by Lukáš Korba on 16.06.2022.
//
import Combine
import XCTest
@testable import secant_testnet
import ComposableArchitecture
@ -87,9 +88,9 @@ class DeeplinkTests: XCTestCase {
store.dependencies.deeplink = DeeplinkClient(
resolveDeeplinkURL: { _, _ in Deeplink.Destination.home }
)
let synchronizer = NoopSDKSynchronizer()
synchronizer.updateStateChanged(.synced)
store.dependencies.sdkSynchronizer = synchronizer
store.dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(
stateChangedStream: { CurrentValueSubject<SDKSynchronizerState, Never>(.synced) }
)
store.dependencies.walletConfigProvider = .noOp
guard let url = URL(string: "zcash:///home") else {
@ -116,9 +117,6 @@ class DeeplinkTests: XCTestCase {
}
func testDeeplinkRequest_Received_Send() async throws {
let synchronizer = NoopSDKSynchronizer()
synchronizer.updateStateChanged(.synced)
var appState = RootReducer.State.placeholder
appState.destinationState.destination = .welcome
appState.appInitializationState = .initialized
@ -130,7 +128,9 @@ class DeeplinkTests: XCTestCase {
dependencies.deeplink = DeeplinkClient(
resolveDeeplinkURL: { _, _ in Deeplink.Destination.send(amount: 123_000_000, address: "address", memo: "some text") }
)
dependencies.sdkSynchronizer = synchronizer
dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(
stateChangedStream: { CurrentValueSubject<SDKSynchronizerState, Never>(.synced) }
)
}
guard let url = URL(string: "zcash:///home/send?address=address&memo=some%20text&amount=123000000") else {

View File

@ -5,6 +5,7 @@
// Created by Lukáš Korba on 02.06.2022.
//
import Combine
import XCTest
import ComposableArchitecture
@testable import secant_testnet
@ -16,6 +17,11 @@ class HomeTests: XCTestCase {
initialState: .placeholder,
reducer: HomeReducer()
)
store.dependencies.sdkSynchronizer = .mocked(
stateChangedStream: { CurrentValueSubject<SDKSynchronizerState, Never>(.progressUpdated) },
statusSnapshot: { .default }
)
store.send(.synchronizerStateChanged(.progressUpdated))
@ -32,7 +38,7 @@ class HomeTests: XCTestCase {
reducer: HomeReducer()
) { dependencies in
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { .default })
}
store.send(.synchronizerStateChanged(.synced))
@ -71,7 +77,7 @@ class HomeTests: XCTestCase {
)
store.dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
store.dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(mockSnapshot)
store.dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { mockSnapshot })
store.send(.synchronizerStateChanged(.progressUpdated))
@ -112,7 +118,7 @@ class HomeTests: XCTestCase {
)
store.dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
store.dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(mockSnapshot)
store.dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { mockSnapshot })
store.send(.updateDestination(.send)) {
$0.destination = .send
@ -135,9 +141,13 @@ class HomeTests: XCTestCase {
let store = TestStore(
initialState: .placeholder,
reducer: HomeReducer()
) {
$0.diskSpaceChecker = .mockEmptyDisk
}
)
store.dependencies.diskSpaceChecker = .mockEmptyDisk
store.dependencies.sdkSynchronizer = .mocked(
stateChangedStream: { CurrentValueSubject<SDKSynchronizerState, Never>(.unknown) },
statusSnapshot: { .default }
)
store.send(.onAppear) { state in
state.requiredTransactionConfirmations = 10
@ -181,15 +191,11 @@ class HomeTests: XCTestCase {
state: .error(testError)
)
let mockSynchronizer = MockSDKSynchronizerClient(
snapshot: errorSnapshot
)
let store = TestStore(
initialState: .placeholder,
reducer: HomeReducer()
) {
$0.sdkSynchronizer = mockSynchronizer
$0.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { errorSnapshot })
}
store.send(.updateSynchronizerStatus) {

View File

@ -20,7 +20,7 @@ class ProfileTests: XCTestCase {
reducer: ProfileReducer()
) { dependencies in
dependencies.appVersion = .mock
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { .default })
}
let uAddress = try UnifiedAddress(

View File

@ -194,6 +194,7 @@ class RecoveryPhraseValidationFlowFeatureFlagTests: XCTestCase {
store.dependencies.walletStorage.exportWallet = { .placeholder }
store.dependencies.walletStorage.areKeysPresent = { true }
store.dependencies.walletConfigProvider = .noOp
store.dependencies.sdkSynchronizer = .noOp
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))

View File

@ -101,6 +101,7 @@ class AppInitializationTests: XCTestCase {
store.dependencies.walletStorage.exportWallet = { .placeholder }
store.dependencies.walletStorage.areKeysPresent = { true }
store.dependencies.walletConfigProvider = .noOp
store.dependencies.sdkSynchronizer = .noOp
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))

View File

@ -72,6 +72,7 @@ class DebugTests: XCTestCase {
)
store.dependencies.mainQueue = .immediate
store.dependencies.sdkSynchronizer = .noOp
await store.send(.debug(.quickRescan)) { state in
state.destinationState.internalDestination = .home
@ -100,6 +101,7 @@ class DebugTests: XCTestCase {
)
store.dependencies.mainQueue = .immediate
store.dependencies.sdkSynchronizer = .noOp
await store.send(.debug(.fullRescan)) { state in
state.destinationState.internalDestination = .home

View File

@ -50,7 +50,7 @@ class SendTests: XCTestCase {
dependencies.derivationTool = .liveValue
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
dependencies.mnemonic = .liveValue
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { .default })
dependencies.walletStorage = .noOp
}
@ -122,7 +122,7 @@ class SendTests: XCTestCase {
dependencies.derivationTool = .liveValue
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
dependencies.mnemonic = .liveValue
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { .default })
dependencies.walletStorage = .noOp
}
@ -190,6 +190,7 @@ class SendTests: XCTestCase {
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
dependencies.mnemonic = .liveValue
dependencies.walletStorage = .noOp
dependencies.sdkSynchronizer = .noOp
}
// simulate the sending confirmation button to be pressed
@ -629,6 +630,8 @@ class SendTests: XCTestCase {
reducer: SendFlowReducer()
)
store.dependencies.sdkSynchronizer = .noOp
store.send(.onAppear) { state in
state.memoState.charLimit = 512
}

View File

@ -22,6 +22,7 @@ class BalanceBreakdownSnapshotTests: XCTestCase {
transparentBalance: WalletBalance(verified: Zatoshi(850_000_000), total: Zatoshi(850_000_000)).redacted
),
reducer: BalanceBreakdownReducer()
.dependency(\.sdkSynchronizer, .noOp)
)
addAttachments(BalanceBreakdownView(store: store))

View File

@ -49,6 +49,7 @@ class HomeSnapshotTests: XCTestCase {
),
reducer: HomeReducer()
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
.dependency(\.sdkSynchronizer, .noOp)
)
// landing home screen

View File

@ -16,7 +16,7 @@ class ProfileSnapshotTests: XCTestCase {
initialState: .placeholder,
reducer: ProfileReducer()
.dependency(\.appVersion, .mock)
.dependency(\.sdkSynchronizer, NoopSDKSynchronizer())
.dependency(\.sdkSynchronizer, .noOp)
)
ViewStore(store).send(.onAppear)

View File

@ -36,6 +36,7 @@ class TransactionSendingTests: XCTestCase {
.dependency(\.mainQueue, DispatchQueue.main.eraseToAnyScheduler())
.dependency(\.numberFormatter, .live())
.dependency(\.walletStorage, .live())
.dependency(\.sdkSynchronizer, .mock)
)
ViewStore(store).send(.onAppear)

View File

@ -16,7 +16,7 @@ class SettingsSnapshotTests: XCTestCase {
initialState: .placeholder,
reducer: SettingsReducer()
.dependency(\.localAuthentication, .mockAuthenticationFailed)
.dependency(\.sdkSynchronizer, NoopSDKSynchronizer())
.dependency(\.sdkSynchronizer, .noOp)
.dependency(\.walletStorage, .noOp)
.dependency(\.appVersion, .mock)
)
@ -29,7 +29,7 @@ class SettingsSnapshotTests: XCTestCase {
initialState: .placeholder,
reducer: SettingsReducer()
.dependency(\.localAuthentication, .mockAuthenticationFailed)
.dependency(\.sdkSynchronizer, NoopSDKSynchronizer())
.dependency(\.sdkSynchronizer, .noOp)
.dependency(\.walletStorage, .noOp)
.dependency(\.appVersion, .liveValue)
)

View File

@ -15,6 +15,8 @@ class WalletEventsSnapshotTests: XCTestCase {
let store = WalletEventsFlowStore(
initialState: .placeHolder,
reducer: WalletEventsFlowReducer()
.dependency(\.sdkSynchronizer, .mock)
.dependency(\.mainQueue, .immediate)
)
// landing wallet events screen

View File

@ -9,81 +9,9 @@ import XCTest
import ZcashLightClientKit
@testable import secant_testnet
extension DatabaseFiles.DatabaseFilesError {
var debugValue: String {
switch self {
case .getFsBlockDbRoot: return "getFsBlockDbRoot"
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"
}
}
}
class DatabaseFilesTests: XCTestCase {
let network = ZcashNetworkBuilder.network(for: .testnet)
func testFailingDocumentsDirectory() throws {
let mockedFileManager = FileManagerClient(
url: { _, _, _, _ in throw "some error" },
fileExists: { _ in return true },
removeItem: { _ in }
)
let dfInteractor = DatabaseFilesClient.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager))
do {
_ = try dfInteractor.documentsDirectory()
XCTFail("DatabaseFiles: `testFailingDocumentsDirectory` expected to fail but passed with no error.")
} catch {
guard let error = error as? DatabaseFiles.DatabaseFilesError else {
XCTFail("DatabaseFiles: the error is expected to be DatabaseFilesError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
DatabaseFiles.DatabaseFilesError.getDocumentsURL.debugValue,
"DatabaseFiles: error must be .getDocumentsURL but it's \(error)."
)
}
}
func testFailingDataDbURL() throws {
let mockedFileManager = FileManagerClient(
url: { _, _, _, _ in throw "some error" },
fileExists: { _ in return true },
removeItem: { _ in }
)
let dfInteractor = DatabaseFilesClient.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager))
do {
_ = try dfInteractor.dataDbURLFor(network)
XCTFail("DatabaseFiles: `testFailingDataDbURL` expected to fail but passed with no error.")
} catch {
guard let error = error as? DatabaseFiles.DatabaseFilesError else {
XCTFail("DatabaseFiles: the error is expected to be DatabaseFilesError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
DatabaseFiles.DatabaseFilesError.getDataURL.debugValue,
"DatabaseFiles: error must be .getDataURL but it's \(error)."
)
}
}
func testDatabaseFilesPresent() throws {
let mockedFileManager = FileManagerClient(
url: { _, _, _, _ in .emptyURL },
@ -92,14 +20,8 @@ class DatabaseFilesTests: XCTestCase {
)
let dfInteractor = DatabaseFilesClient.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager))
do {
let areFilesPresent = try dfInteractor.areDbFilesPresentFor(network)
XCTAssertTrue(areFilesPresent, "DatabaseFiles: `testDatabaseFilesPresent` is expected to be true but it's \(areFilesPresent)")
} catch {
XCTFail("DatabaseFiles: `testDatabaseFilesPresent` expected to fail but passed with no error.")
}
let areFilesPresent = dfInteractor.areDbFilesPresentFor(network)
XCTAssertTrue(areFilesPresent, "DatabaseFiles: `testDatabaseFilesPresent` is expected to be true but it's \(areFilesPresent)")
}
func testDatabaseFilesNotPresent() throws {
@ -110,97 +32,7 @@ class DatabaseFilesTests: XCTestCase {
)
let dfInteractor = DatabaseFilesClient.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager))
do {
let areFilesPresent = try dfInteractor.areDbFilesPresentFor(network)
XCTAssertFalse(areFilesPresent, "DatabaseFiles: `testDatabaseFilesNotPresent` is expected to be false but it's \(areFilesPresent)")
} catch {
XCTFail("DatabaseFiles: `testDatabaseFilesPresent` expected to fail but passed with no error.")
}
}
func testDatabaseFilesPresentFailure() throws {
let mockedFileManager = FileManagerClient(
url: { _, _, _, _ in throw "some error" },
fileExists: { _ in return true },
removeItem: { _ in }
)
let dfInteractor = DatabaseFilesClient.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager))
do {
_ = try dfInteractor.areDbFilesPresentFor(network)
XCTFail("DatabaseFiles: `testDatabaseFilesPresentFailure` expected to fail but passed with no error.")
} catch {
guard let error = error as? DatabaseFiles.DatabaseFilesError else {
XCTFail("DatabaseFiles: the error is expected to be DatabaseFilesError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
DatabaseFiles.DatabaseFilesError.filesPresentCheck.debugValue,
"DatabaseFiles: error must be .filesPresentCheck but it's \(error)."
)
}
}
func testNukeFiles_RemoveFileFailure() throws {
let mockedFileManager = FileManagerClient(
url: { _, _, _, _ in .emptyURL },
fileExists: { _ in return true },
removeItem: { _ in throw "some error" }
)
let dfInteractor = DatabaseFilesClient.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager))
do {
_ = try dfInteractor.nukeDbFilesFor(network)
XCTFail("DatabaseFiles: `testNukeFiles_RemoveFileFailure` expected to fail but passed with no error.")
} catch {
guard let error = error as? DatabaseFiles.DatabaseFilesError else {
XCTFail("DatabaseFiles: the error is expected to be DatabaseFilesError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
DatabaseFiles.DatabaseFilesError.nukeFiles.debugValue,
"DatabaseFiles: error must be .nukeFiles but it's \(error)."
)
}
}
func testNukeFiles_URLFailure() throws {
let mockedFileManager = FileManagerClient(
url: { _, _, _, _ in throw "some error" },
fileExists: { _ in return true },
removeItem: { _ in }
)
let dfInteractor = DatabaseFilesClient.live(databaseFiles: DatabaseFiles(fileManager: mockedFileManager))
do {
_ = try dfInteractor.nukeDbFilesFor(network)
XCTFail("DatabaseFiles: `testNukeFiles_URLFailure` expected to fail but passed with no error.")
} catch {
guard let error = error as? DatabaseFiles.DatabaseFilesError else {
XCTFail("DatabaseFiles: the error is expected to be DatabaseFilesError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
DatabaseFiles.DatabaseFilesError.nukeFiles.debugValue,
"DatabaseFiles: error must be .nukeFiles but it's \(error)."
)
}
let areFilesPresent = dfInteractor.areDbFilesPresentFor(network)
XCTAssertFalse(areFilesPresent, "DatabaseFiles: `testDatabaseFilesNotPresent` is expected to be false but it's \(areFilesPresent)")
}
}

View File

@ -22,6 +22,8 @@ class WalletEventsTests: XCTestCase {
),
reducer: WalletEventsFlowReducer()
)
store.dependencies.sdkSynchronizer = .noOp
store.send(.onAppear) { state in
state.requiredTransactionConfirmations = 10
@ -78,10 +80,12 @@ class WalletEventsTests: XCTestCase {
reducer: WalletEventsFlowReducer()
) { dependencies in
dependencies.mainQueue = Self.testScheduler.eraseToAnyScheduler()
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
dependencies.sdkSynchronizer = SDKSynchronizerClient.mocked(statusSnapshot: { .default })
}
store.send(.synchronizerStateChanged(.synced))
store.send(.synchronizerStateChanged(.synced)) { state in
state.latestMinedHeight = 0
}
Self.testScheduler.advance(by: 0.01)