[#982] Background syncing (#985)

Background processing task implemented
Doc for the Background Synchronization added
changelog update
custom wifi check
This commit is contained in:
Lukas Korba 2024-01-08 14:14:48 +01:00 committed by GitHub
parent 79205018a5
commit 1d60dd3275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 356 additions and 54 deletions

70
BACKGROUND_SYNCING.md Normal file
View File

@ -0,0 +1,70 @@
# Background Syncing
## Sources
We encourange you to watch WWDC videos:
- [Advances in App Background Execution]: https://developer.apple.com/videos/play/wwdc2019/707
- [Background execution demystified]: https://developer.apple.com/videos/play/wwdc2020/10063
## Implementation details
There are 4 different APIs and types of background tasks. Each one is specific and can be used for different scenarios. Synchronization of the blockchain data is time and memory consuming operation. Therefore the `BGProcessingTask` has been used. This type of task is designed to run for a longer time when certain conditions are met (watch Background execution demystified).
### Steps to make it work
1. Add a capability of `background modes` in the settings of the xcode project.
2. Turn the `Background Processing` mode on in the new capability.
3. Add `Permitted background task scheduler identifiers` to the info.plist.
4. Create the ID for the background task in the newly created array.
5. Register the BGTask in `application.didFinishLaunchingWithOptions`
```Swift
BGTaskScheduler.shared.register(
forTaskWithIdentifier: <ID>,
using: DispatchQueue.main
) { task in
// see the next steps
}
```
Note: The queue is an optional and most tutorials leave the parameter `nil` but Zashi requires main thread processing due to UI layer - therefore we pass `DispatchQueue.main`.
6. Call a method that schedules the task execution.
7. Start the synchronizer.
8. Set the expiration closure and stop the synchronizer inside it
```swift
task.expirationHandler = {
synchronizer.stop()
}
```
9. The body of the registered task summarized:
```swift
BGTaskScheduler.shared.register(...) { task in
scheduleTask() // 6
synchronizer.start() // 7
task.expirationHandler = {
synchronizer.stop() // 8
}
}
```
10. Call `scheduleTask()` when app goes to the background so there is the initial scheduling done, the next one will be handled by the closure of the registered task. The method usually consists of:
```Swift
let request = BGProcessingTaskRequest(identifier: <ID>)
request.earliestBeginDate = <scheduledTime>
request.requiresExternalPower = true // optional, we require the iPhone to be connected to the power
request.requiresNetworkConnectivity = true // required
do {
try BGTaskScheduler.shared.submit(request)
} catch { // handle error }
```
11. Last step is to call `.setTaskCompleted(success: <bool>)` on the BGTask when the work is done. This is required by the system no matter what. We call it with `true` when the synchronizer finishes the work (up-to-date state) and with `false` for other or failed reasons (stopped state, error state, etc.).
You can see specific details of the Zashi implementation in:
- Xcode project settings, steps 1-4.
- AppDelegate.swift file, steps 5-9.
- SecantApp.swift file, step 10.
- RootInitialization.swift, step 11.
## Gotchas
- The `requiresNetworkConnectivity` flag doesn't specify or deal with the type of connectivity. It simply allows scheduling when the iPhone is connected to the internet. We deal with it when the task is triggered. The custom check wheather the wifi is or is not connected preceeds the start of the synchronizer.
- When the app is killed by a user in the app switcher, the scheduled BGTask is deleted. So the BGTask is triggered at the scheduled time only when the app is suspended or killed by the system. Explicit termination of the app by a user leads to termination of any background processing.

View File

@ -8,6 +8,7 @@ directly impact users rather than highlighting other crucial architectural updat
### Added
- The exported logs also show the shielded balances (total & verified) for every finished sync metric.
- Synchronization in the background. When the iPhone is connected to the power and wifi, the background task will try to synchronize randomly between 3-4am.
### Fixed
- The export buttons are disabled when exporting of the private data is in progress.

View File

@ -105,7 +105,7 @@ extension RootReducer {
state.splashAppeared = true
return .none
case .tabs, .initialization, .onboarding, .sandbox, .updateStateAfterConfigUpdate, .alert, .phraseDisplay,
case .tabs, .initialization, .onboarding, .sandbox, .updateStateAfterConfigUpdate, .alert, .phraseDisplay, .synchronizerStateChanged,
.welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .exportLogs, .confirmationDialog:
return .none
}

View File

@ -28,6 +28,7 @@ extension RootReducer {
case nukeWalletRequest
case respondToWalletInitializationState(InitializationState)
case synchronizerStartFailed(ZcashError)
case registerForSynchronizersUpdate
case retryStart
case walletConfigChanged(WalletConfig)
}
@ -38,7 +39,15 @@ extension RootReducer {
switch action {
case .initialization(.appDelegate(.didEnterBackground)):
sdkSynchronizer.stop()
return .none
state.bgTask?.setTaskCompleted(success: false)
state.bgTask = nil
return .cancel(id: CancelStateId.timer)
case .initialization(.appDelegate(.backgroundTask(let task))):
state.bgTask = task
return .run { send in
await send(.initialization(.retryStart))
}
case .initialization(.appDelegate(.willEnterForeground)):
return .run { send in
@ -46,6 +55,37 @@ extension RootReducer {
await send(.initialization(.retryStart))
}
case .synchronizerStateChanged(let latestState):
guard state.bgTask != nil else {
return .none
}
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.syncStatus)
var finishBGTask = false
var successOfBGTask = false
switch snapshot.syncStatus {
case .upToDate:
successOfBGTask = true
finishBGTask = true
case .stopped, .error:
successOfBGTask = false
finishBGTask = true
default: break
}
LoggerProxy.event("BGTask .synchronizerStateChanged(let latestState): \(snapshot.syncStatus)")
if finishBGTask {
LoggerProxy.event("BGTask setTaskCompleted(success: \(successOfBGTask)) from TCA")
state.bgTask?.setTaskCompleted(success: successOfBGTask)
state.bgTask = nil
return .cancel(id: CancelStateId.timer)
}
return .none
case .initialization(.synchronizerStartFailed):
return .none
@ -54,13 +94,28 @@ extension RootReducer {
guard sdkSynchronizer.latestState().syncStatus.isPrepared else {
return .none
}
return .run { send in
return .run { [state] send in
do {
try await sdkSynchronizer.start(true)
if state.bgTask != nil {
LoggerProxy.event("BGTask synchronizer.start() PASSED")
}
await send(.initialization(.registerForSynchronizersUpdate))
} catch {
if state.bgTask != nil {
LoggerProxy.event("BGTask synchronizer.start() failed \(error.toZcashError())")
}
await send(.initialization(.synchronizerStartFailed(error.toZcashError())))
}
}
case .initialization(.registerForSynchronizersUpdate):
return .publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map(RootReducer.Action.synchronizerStateChanged)
}
.cancellable(id: CancelStateId.timer, cancelInFlight: true)
case .initialization(.appDelegate(.didFinishLaunching)):
// TODO: [#704], trigger the review request logic when approved by the team,
@ -174,7 +229,7 @@ extension RootReducer {
case .initialization(.initializationSuccessfullyDone(let uAddress)):
state.tabsState.addressDetailsState.uAddress = uAddress
return .none
return .send(.initialization(.registerForSynchronizersUpdate))
case .initialization(.checkBackupPhraseValidation):
guard let storedWallet = state.storedWallet else {

View File

@ -17,12 +17,14 @@ import Tabs
import CrashReporter
import ReadTransactionsStorage
import RecoveryPhraseDisplay
import BackgroundTasks
public typealias RootStore = Store<RootReducer.State, RootReducer.Action>
public typealias RootViewStore = ViewStore<RootReducer.State, RootReducer.Action>
public struct RootReducer: Reducer {
enum CancelId { case timer }
enum CancelStateId { case timer }
enum SynchronizerCancelId { case timer }
enum WalletConfigCancelId { case timer }
let tokenName: String
@ -31,6 +33,7 @@ public struct RootReducer: Reducer {
public struct State: Equatable {
@PresentationState public var alert: AlertState<Action>?
public var appInitializationState: InitializationState = .uninitialized
public var bgTask: BGProcessingTask?
@PresentationState public var confirmationDialog: ConfirmationDialogState<Action.ConfirmationDialog>?
public var debugState: DebugState
public var destinationState: DestinationState
@ -92,6 +95,7 @@ public struct RootReducer: Reducer {
case splashFinished
case splashRemovalRequested
case sandbox(SandboxReducer.Action)
case synchronizerStateChanged(SynchronizerState)
case updateStateAfterConfigUpdate(WalletConfig)
case walletConfigLoaded(WalletConfig)
case welcome(WelcomeReducer.Action)

View File

@ -1,14 +1,16 @@
//
// AppDelegate.swift
// AppDelegateAction.swift
// secant-testnet
//
// Created by Lukáš Korba on 27.03.2022.
//
import Foundation
import BackgroundTasks
public enum AppDelegateAction: Equatable {
case didFinishLaunching
case didEnterBackground
case willEnterForeground
case backgroundTask(BGProcessingTask)
}

View File

@ -83,6 +83,8 @@
9EB35D632A31F1DD00A2149B /* Root in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D622A31F1DD00A2149B /* Root */; };
9EB35D6A2A3A2D7B00A2149B /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D692A3A2D7B00A2149B /* Utils */; };
9EB35D6C2A3A2D9200A2149B /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D6B2A3A2D9200A2149B /* Utils */; };
9EEB06C82B405A0400EEE50F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEB06C72B405A0400EEE50F /* AppDelegate.swift */; };
9EEB06C92B405A0400EEE50F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEB06C72B405A0400EEE50F /* AppDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -168,6 +170,7 @@
9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencySelectionTests.swift; sourceTree = "<group>"; };
9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputTests.swift; sourceTree = "<group>"; };
9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputTests.swift; sourceTree = "<group>"; };
9EEB06C72B405A0400EEE50F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; };
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -242,6 +245,7 @@
isa = PBXGroup;
children = (
9E7FE0B6282D1D9800C374E8 /* Resources */,
9EEB06C72B405A0400EEE50F /* AppDelegate.swift */,
0D4E7A0826B364170058B01E /* SecantApp.swift */,
9E2F1C8B280ED6A7004E65FE /* LaunchScreen.storyboard */,
0DEF4766299EA5920032708B /* secant-mainnet-Info.plist */,
@ -910,6 +914,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9EEB06C92B405A0400EEE50F /* AppDelegate.swift in Sources */,
0D26AF24299E8196005260EE /* SecantApp.swift in Sources */,
9E2706682AFF99F5000DA6EC /* ReadTransactionsStorageModel.xcdatamodeld in Sources */,
);
@ -919,6 +924,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9EEB06C82B405A0400EEE50F /* AppDelegate.swift in Sources */,
0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */,
9E2706672AFF99F5000DA6EC /* ReadTransactionsStorageModel.xcdatamodeld in Sources */,
);
@ -1011,7 +1017,7 @@
CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
DEVELOPMENT_TEAM = W5KABFU8SV;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "secant/secant-mainnet-Info.plist";
@ -1039,7 +1045,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
DEVELOPMENT_TEAM = W5KABFU8SV;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "secant/secant-mainnet-Info.plist";

189
secant/AppDelegate.swift Normal file
View File

@ -0,0 +1,189 @@
//
// AppDelegate.swift
// secant
//
// Created by Lukáš Korba on 30.12.2023.
//
import SwiftUI
import ComposableArchitecture
import ZcashLightClientKit
import Network
import Utils
import Root
import BackgroundTasks
final class AppDelegate: NSObject, UIApplicationDelegate {
private let bcgTaskId = "co.electriccoin.power_wifi_sync"
private let bcgSchedulerTaskId = "co.electriccoin.scheduler"
private var monitor: NWPathMonitor?
private let workerQueue = DispatchQueue(label: "Monitor")
private var isConnectedToWifi = false
let rootStore = RootStore(
initialState: .initial
) {
RootReducer(
tokenName: TargetConstants.tokenName,
zcashNetwork: TargetConstants.zcashNetwork
).logging()
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
#if DEBUG
// Short-circuit if running unit tests to avoid side-effects from the app running.
guard !_XCTIsTesting else { return true }
#endif
walletLogger = OSLogger(logLevel: .debug, category: LoggerConstants.walletLogs)
handleBackgroundTask()
return true
}
func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
}
}
// MARK: - BackgroundTasks
extension AppDelegate {
private func handleBackgroundTask() {
// We require the background task to run when connected to the power and wifi
monitor = NWPathMonitor(requiredInterfaceType: .wifi)
monitor?.pathUpdateHandler = { [weak self] path in
if path.status == .satisfied {
self?.isConnectedToWifi = true
} else {
self?.isConnectedToWifi = false
}
LoggerProxy.event("BGTask isConnectedToWifi \(path.status == .satisfied)")
}
monitor?.start(queue: workerQueue)
// set the default behavior for the NSDecimalNumber
NSDecimalNumber.defaultBehavior = Zatoshi.decimalHandler
rootStore.send(.initialization(.appDelegate(.didFinishLaunching)))
registerTasks()
}
private func registerTasks() {
let bcgSyncTaskResult = BGTaskScheduler.shared.register(
forTaskWithIdentifier: bcgTaskId,
using: DispatchQueue.main
) { [self] task in
LoggerProxy.event("BGTask BGTaskScheduler.shared.register SYNC called")
guard let task = task as? BGProcessingTask else {
return
}
startBackgroundTask(task)
}
LoggerProxy.event("BGTask SYNC registered \(bcgSyncTaskResult)")
let bcgSchedulerTaskResult = BGTaskScheduler.shared.register(
forTaskWithIdentifier: bcgSchedulerTaskId,
using: DispatchQueue.main
) { [self] task in
LoggerProxy.event("BGTask BGTaskScheduler.shared.register SCHEDULER called")
guard let task = task as? BGProcessingTask else {
return
}
scheduleSchedulerBackgroundTask()
scheduleBackgroundTask()
task.setTaskCompleted(success: true)
}
LoggerProxy.event("BGTask SCHEDULER registered \(bcgSchedulerTaskResult)")
}
private func startBackgroundTask(_ task: BGProcessingTask) {
LoggerProxy.event("BGTask startBackgroundTask called")
// schedule tasks for the next time
scheduleBackgroundTask()
scheduleSchedulerBackgroundTask()
guard isConnectedToWifi else {
LoggerProxy.event("BGTask startBackgroundTask: not connected to the wifi")
task.setTaskCompleted(success: false)
return
}
// start the syncing
rootStore.send(.initialization(.appDelegate(.backgroundTask(task))))
task.expirationHandler = { [rootStore] in
LoggerProxy.event("BGTask startBackgroundTask expirationHandler called")
// stop the syncing because the allocated time is about to expire
rootStore.send(.initialization(.appDelegate(.didEnterBackground)))
}
}
func scheduleBackgroundTask() {
// This method can be called as many times as needed, the previously submitted
// request will be overridden by the new one.
LoggerProxy.event("BGTask scheduleBackgroundTask called")
let request = BGProcessingTaskRequest(identifier: bcgTaskId)
let today = Calendar.current.startOfDay(for: .now)
guard let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today) else {
LoggerProxy.event("BGTask scheduleBackgroundTask failed to schedule time")
return
}
let earlyMorningComponent = DateComponents(hour: 3, minute: Int.random(in: 0...60))
let earlyMorning = Calendar.current.date(byAdding: earlyMorningComponent, to: tomorrow)
request.earliestBeginDate = earlyMorning
request.requiresExternalPower = true
request.requiresNetworkConnectivity = true
do {
try BGTaskScheduler.shared.submit(request)
LoggerProxy.event("BGTask scheduleBackgroundTask succeeded to submit")
} catch {
LoggerProxy.event("BGTask scheduleBackgroundTask failed to submit, error: \(error)")
}
}
func scheduleSchedulerBackgroundTask() {
// This method can be called as many times as needed, the previously submitted
// request will be overridden by the new one.
LoggerProxy.event("BGTask scheduleSchedulerBackgroundTask called")
let request = BGProcessingTaskRequest(identifier: bcgSchedulerTaskId)
let today = Calendar.current.startOfDay(for: .now)
guard let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today) else {
LoggerProxy.event("BGTask scheduleSchedulerBackgroundTask failed to schedule time")
return
}
let afternoonComponent = DateComponents(hour: 14, minute: Int.random(in: 0...60))
let afternoon = Calendar.current.date(byAdding: afternoonComponent, to: tomorrow)
request.earliestBeginDate = afternoon
request.requiresExternalPower = false
request.requiresNetworkConnectivity = false
do {
try BGTaskScheduler.shared.submit(request)
LoggerProxy.event("BGTask scheduleSchedulerBackgroundTask succeeded to submit")
} catch {
LoggerProxy.event("BGTask scheduleSchedulerBackgroundTask failed to submit, error: \(error)")
}
}
}

View File

@ -12,42 +12,6 @@ import ZcashLightClientKit
import SDKSynchronizer
import Utils
import Root
import XCTestDynamicOverlay
final class AppDelegate: NSObject, UIApplicationDelegate {
let rootStore = RootStore(
initialState: .initial
) {
RootReducer(
tokenName: TargetConstants.tokenName,
zcashNetwork: TargetConstants.zcashNetwork
).logging()
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
#if DEBUG
// Short-circuit if running unit tests to avoid side-effects from the app running.
guard !_XCTIsTesting else { return true }
#endif
walletLogger = OSLogger(logLevel: .debug, category: LoggerConstants.walletLogs)
// set the default behavior for the NSDecimalNumber
NSDecimalNumber.defaultBehavior = Zatoshi.decimalHandler
rootStore.send(.initialization(.appDelegate(.didFinishLaunching)))
return true
}
func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
}
}
@main
struct SecantApp: App {
@ -74,6 +38,8 @@ struct SecantApp: App {
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
appDelegate.rootStore.send(.initialization(.appDelegate(.didEnterBackground)))
appDelegate.scheduleBackgroundTask()
appDelegate.scheduleSchedulerBackgroundTask()
}
.preferredColorScheme(.light)
}

View File

@ -4,8 +4,8 @@
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>co.electriccoin.backgroundProcessingTask</string>
<string>co.electriccoin.backgroundAppRefreshTask</string>
<string>co.electriccoin.power_wifi_sync</string>
<string>co.electriccoin.scheduler</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
@ -46,11 +46,6 @@
<string>Scan zAddress Qr Codes</string>
<key>NSFaceIDUsageDescription</key>
<string>To access sensitive wallet data which should never be shared with anyone. For example backup phrase seed that can restore the wallet and gain access to funds.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIAppFonts</key>
<array>
<string>Zboto.otf</string>
@ -91,6 +86,11 @@
<string>Archivo-ExtraLightItalic.otf</string>
<string>Archivo-ExtraLight.otf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>

View File

@ -4,8 +4,8 @@
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>co.electriccoin.backgroundProcessingTask</string>
<string>co.electriccoin.backgroundAppRefreshTask</string>
<string>co.electriccoin.power_wifi_sync</string>
<string>co.electriccoin.scheduler</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>

View File

@ -82,6 +82,8 @@ class AppInitializationTests: XCTestCase {
await store.receive(.initialization(.initializationSuccessfullyDone(nil)))
await store.receive(.initialization(.registerForSynchronizersUpdate))
await store.receive(.destination(.updateDestination(.tabs))) { state in
state.destinationState.previousDestination = .welcome
state.destinationState.internalDestination = .tabs
@ -158,6 +160,8 @@ class AppInitializationTests: XCTestCase {
await store.receive(.initialization(.initializationSuccessfullyDone(nil)))
await store.receive(.initialization(.registerForSynchronizersUpdate))
await store.receive(.destination(.updateDestination(.phraseDisplay))) { state in
state.destinationState.previousDestination = .welcome
state.destinationState.internalDestination = .phraseDisplay

View File

@ -208,15 +208,20 @@ class RootTests: XCTestCase {
RootReducer(tokenName: "ZEC", zcashNetwork: ZcashNetworkBuilder.network(for: .testnet))
}
store.dependencies.mainQueue = .immediate
store.dependencies.sdkSynchronizer = .noOp
// swiftlint:disable line_length
let uAddress = try UnifiedAddress(
encoding: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h", network: .testnet
)
await store.send(.initialization(.initializationSuccessfullyDone(uAddress))) { state in
state.tabsState.addressDetailsState.uAddress = uAddress
}
await store.receive(.initialization(.registerForSynchronizersUpdate))
await store.finish()
}
}