Closes #895 Add Sync Session ID to `SynchronizerState`. A SyncSession is an attempt to sync the blockchain within the lifetime of a Synchronizer. A Synchronizer can sync many times, when synced it will refresh every ~20 seconds +- random padding. each sync attempt will have a different UUID even if it's from the same instance of SDKSynchronizer. **How are SyncSessions are delimited? With `SyncStatus` changes.** changes from [`.unprepared`|`.error`|`.disconnected`|`.stopped`] to `.syncing` assign a new sessionID to the synchronizer. Any other transitions won't. `areTwoStatusesDifferent ` was refactored to a helper function of `SyncStatus` **How are IDs generated?** ID generation is not mandated but delegated to a protocol `SyncSessionIDGenerator`. Tests inject their own deterministic generator to avoid test flakiness. Default implementation of SyncSessionIDGenerator is ````Swift struct UniqueSyncSessionIDGenerator {} extension UniqueSyncSessionIDGenerator: SyncSessionIDGenerator { func nextID() -> UUID { UUID() } } ```` **SyncSession Pseudo-Atomicity and thread safety** SyncSession is a type alias of a GenericActor holding a UUID ```` typealias SyncSession = GenericActor<UUID> extension SyncSession { /// updates the current sync session to a new value with the given generator /// - Parameters generator: a `SyncSessionIDGenerator` /// - returns: the `UUID` of the newly updated value. @discardableResult func newSession(with generator: SyncSessionIDGenerator) async -> UUID { return await self.update(generator.nextID()) } } ```` Closes #895 SessionTicker struct to control session transitions. switching to `.unprepared` now makes syncID to be `.nullID`
This commit is contained in:
parent
e3bc06b694
commit
6b7fbdd908
|
@ -1108,8 +1108,8 @@ actor CompactBlockProcessor {
|
|||
timeInterval: interval,
|
||||
repeats: true,
|
||||
block: { [weak self] _ in
|
||||
Task { [self] in
|
||||
guard let self = self else { return }
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
if await self.shouldStart {
|
||||
self.logger.debug(
|
||||
"""
|
||||
|
|
|
@ -64,15 +64,27 @@ public enum ConnectionState {
|
|||
/// the connection has been closed
|
||||
case shutdown
|
||||
}
|
||||
|
||||
/// Reports the state of a synchronizer.
|
||||
public struct SynchronizerState: Equatable {
|
||||
/// Unique Identifier for the current sync attempt
|
||||
/// - Note: Although on it's lifetime a synchronizer will attempt to sync between random fractions of a minute (when idle),
|
||||
/// each sync attempt will be considered a new sync session. This is to maintain a consistent UUID cadence
|
||||
/// given how application lifecycle varies between OS Versions, platforms, etc.
|
||||
/// SyncSessionIDs are provided to users
|
||||
public var syncSessionID: UUID
|
||||
/// shielded balance known to this synchronizer given the data that has processed locally
|
||||
public var shieldedBalance: WalletBalance
|
||||
/// transparent balance known to this synchronizer given the data that has processed locally
|
||||
public var transparentBalance: WalletBalance
|
||||
/// status of the whole sync process
|
||||
public var syncStatus: SyncStatus
|
||||
/// height of the latest scanned block known to this synchronizer.
|
||||
public var latestScannedHeight: BlockHeight
|
||||
|
||||
/// Represents a synchronizer that has made zero progress hasn't done a sync attempt
|
||||
public static var zero: SynchronizerState {
|
||||
SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
shieldedBalance: .zero,
|
||||
transparentBalance: .zero,
|
||||
syncStatus: .unprepared,
|
||||
|
@ -423,3 +435,10 @@ extension SyncStatus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UUID {
|
||||
/// UUID 00000000-0000-0000-0000-000000000000
|
||||
static var nullID: UUID {
|
||||
UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,9 @@ public class SDKSynchronizer: Synchronizer {
|
|||
private var transactionRepository: TransactionRepository
|
||||
private var utxoRepository: UnspentTransactionOutputRepository
|
||||
|
||||
private var syncSessionIDGenerator: SyncSessionIDGenerator
|
||||
private var syncSession: SyncSession
|
||||
private var syncSessionTicker: SessionTicker
|
||||
private var syncStartDate: Date?
|
||||
|
||||
/// Creates an SDKSynchronizer instance
|
||||
|
@ -61,7 +64,9 @@ public class SDKSynchronizer: Synchronizer {
|
|||
logger: initializer.logger,
|
||||
walletBirthdayProvider: { initializer.walletBirthday }
|
||||
),
|
||||
metrics: metrics
|
||||
metrics: metrics,
|
||||
syncSessionIDGenerator: UniqueSyncSessionIDGenerator(),
|
||||
syncSessionTicker: .live
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -72,7 +77,9 @@ public class SDKSynchronizer: Synchronizer {
|
|||
transactionRepository: TransactionRepository,
|
||||
utxoRepository: UnspentTransactionOutputRepository,
|
||||
blockProcessor: CompactBlockProcessor,
|
||||
metrics: SDKMetrics
|
||||
metrics: SDKMetrics,
|
||||
syncSessionIDGenerator: SyncSessionIDGenerator,
|
||||
syncSessionTicker: SessionTicker
|
||||
) {
|
||||
self.connectionState = .idle
|
||||
self.underlyingStatus = GenericActor(status)
|
||||
|
@ -84,7 +91,10 @@ public class SDKSynchronizer: Synchronizer {
|
|||
self.network = initializer.network
|
||||
self.metrics = metrics
|
||||
self.logger = initializer.logger
|
||||
|
||||
self.syncSessionIDGenerator = syncSessionIDGenerator
|
||||
self.syncSession = SyncSession(.nullID)
|
||||
self.syncSessionTicker = syncSessionTicker
|
||||
|
||||
initializer.lightWalletService.connectionStateChange = { [weak self] oldState, newState in
|
||||
self?.connectivityStateChanged(oldState: oldState, newState: newState)
|
||||
}
|
||||
|
@ -572,7 +582,8 @@ public class SDKSynchronizer: Synchronizer {
|
|||
// MARK: notify state
|
||||
|
||||
private func snapshotState(status: SyncStatus) async -> SynchronizerState {
|
||||
SynchronizerState(
|
||||
await SynchronizerState(
|
||||
syncSessionID: syncSession.value,
|
||||
shieldedBalance: WalletBalance(
|
||||
verified: (try? await getShieldedVerifiedBalance()) ?? .zero,
|
||||
total: (try? await getShieldedBalance()) ?? .zero
|
||||
|
@ -592,12 +603,23 @@ public class SDKSynchronizer: Synchronizer {
|
|||
// When new snapshot is created balance is checked. And when balance is checked and data DB doesn't exist then rust initialise new database.
|
||||
// So it's necessary to not create new snapshot after status is switched to `unprepared` otherwise data DB exists after wipe
|
||||
if newStatus == .unprepared {
|
||||
newState = SynchronizerState.zero
|
||||
var nextState = SynchronizerState.zero
|
||||
|
||||
let nextSessionID = await self.syncSession.update(.nullID)
|
||||
|
||||
nextState.syncSessionID = nextSessionID
|
||||
newState = nextState
|
||||
} else {
|
||||
if areTwoStatusesDifferent(firstStatus: oldStatus, secondStatus: newStatus) {
|
||||
if oldStatus.isDifferent(from: newStatus) {
|
||||
if SessionTicker.live.isNewSyncSession(oldStatus, newStatus) {
|
||||
await self.syncSession.newSession(with: self.syncSessionIDGenerator)
|
||||
}
|
||||
|
||||
newState = await snapshotState(status: newStatus)
|
||||
} else {
|
||||
newState = SynchronizerState(
|
||||
|
||||
newState = await SynchronizerState(
|
||||
syncSessionID: syncSession.value,
|
||||
shieldedBalance: latestState.shieldedBalance,
|
||||
transparentBalance: latestState.transparentBalance,
|
||||
syncStatus: newStatus,
|
||||
|
@ -610,20 +632,6 @@ public class SDKSynchronizer: Synchronizer {
|
|||
updateStateStream(with: latestState)
|
||||
}
|
||||
|
||||
private func areTwoStatusesDifferent(firstStatus: SyncStatus, secondStatus: SyncStatus) -> Bool {
|
||||
switch (firstStatus, secondStatus) {
|
||||
case (.unprepared, .unprepared): return false
|
||||
case (.syncing, .syncing): return false
|
||||
case (.enhancing, .enhancing): return false
|
||||
case (.fetching, .fetching): return false
|
||||
case (.synced, .synced): return false
|
||||
case (.stopped, .stopped): return false
|
||||
case (.disconnected, .disconnected): return false
|
||||
case (.error, .error): return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStateStream(with newState: SynchronizerState) {
|
||||
streamsUpdateQueue.async { [weak self] in
|
||||
self?.stateSubject.send(newState)
|
||||
|
@ -739,3 +747,44 @@ extension SDKSynchronizer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncStatus {
|
||||
func isDifferent(from otherStatus: SyncStatus) -> Bool {
|
||||
switch (self, otherStatus) {
|
||||
case (.unprepared, .unprepared): return false
|
||||
case (.syncing, .syncing): return false
|
||||
case (.enhancing, .enhancing): return false
|
||||
case (.fetching, .fetching): return false
|
||||
case (.synced, .synced): return false
|
||||
case (.stopped, .stopped): return false
|
||||
case (.disconnected, .disconnected): return false
|
||||
case (.error, .error): return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionTicker {
|
||||
/// Helper function to determine whether we are in front of a SyncSession change for a given syncStatus
|
||||
/// transition we consider that every sync attempt is a new sync session and should have it's unique UUID reported.
|
||||
var isNewSyncSession: (SyncStatus, SyncStatus) -> Bool
|
||||
}
|
||||
|
||||
extension SessionTicker {
|
||||
static let live = SessionTicker { oldStatus, newStatus in
|
||||
// if the state hasn't changed to a different syncStatus member
|
||||
guard oldStatus.isDifferent(from: newStatus) else { return false }
|
||||
|
||||
switch (oldStatus, newStatus) {
|
||||
case (.unprepared, .syncing):
|
||||
return true
|
||||
case (.error, .syncing),
|
||||
(.disconnected, .syncing),
|
||||
(.stopped, .syncing),
|
||||
(.synced, .syncing):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Francisco Gindre on 3/31/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
protocol SyncSessionIDGenerator {
|
||||
func nextID() -> UUID
|
||||
}
|
||||
|
||||
struct UniqueSyncSessionIDGenerator {}
|
||||
|
||||
extension UniqueSyncSessionIDGenerator: SyncSessionIDGenerator {
|
||||
func nextID() -> UUID {
|
||||
UUID()
|
||||
}
|
||||
}
|
||||
|
||||
typealias SyncSession = GenericActor<UUID>
|
||||
|
||||
extension SyncSession {
|
||||
/// updates the current sync session to a new value with the given generator
|
||||
/// - Parameters generator: a `SyncSessionIDGenerator`
|
||||
/// - returns: the `UUID` of the newly updated value.
|
||||
@discardableResult
|
||||
func newSession(with generator: SyncSessionIDGenerator) async -> UUID {
|
||||
return await self.update(generator.nextID())
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import Combine
|
|||
@testable import TestUtils
|
||||
@testable import ZcashLightClientKit
|
||||
|
||||
class SychronizerDarksideTests: XCTestCase {
|
||||
class SynchronizerDarksideTests: XCTestCase {
|
||||
let sendAmount: Int64 = 1000
|
||||
let defaultLatestHeight: BlockHeight = 663175
|
||||
let branchID = "2bb40e60"
|
||||
|
@ -26,11 +26,11 @@ class SychronizerDarksideTests: XCTestCase {
|
|||
var reorgExpectation = XCTestExpectation(description: "reorg")
|
||||
var foundTransactions: [ZcashTransaction.Overview] = []
|
||||
var cancellables: [AnyCancellable] = []
|
||||
|
||||
var idGenerator: MockSyncSessionIDGenerator!
|
||||
override func setUp() async throws {
|
||||
try await super.setUp()
|
||||
|
||||
self.coordinator = try await TestCoordinator(walletBirthday: birthday, network: network)
|
||||
idGenerator = MockSyncSessionIDGenerator(ids: [.deadbeef])
|
||||
self.coordinator = try await TestCoordinator(walletBirthday: birthday, network: network, syncSessionIDGenerator: idGenerator)
|
||||
try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,8 @@ class SychronizerDarksideTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testFoundManyTransactions() async throws {
|
||||
|
||||
self.idGenerator.ids = [.deadbeef, .beefbeef, .beefdead]
|
||||
coordinator.synchronizer.eventStream
|
||||
.map { event in
|
||||
guard case let .foundTransactions(transactions, _) = event else { return nil }
|
||||
|
@ -142,6 +144,8 @@ class SychronizerDarksideTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testLastStates() async throws {
|
||||
self.idGenerator.ids = [.deadbeef]
|
||||
|
||||
var cancellables: [AnyCancellable] = []
|
||||
|
||||
var states: [SynchronizerState] = []
|
||||
|
@ -169,32 +173,39 @@ class SychronizerDarksideTests: XCTestCase {
|
|||
|
||||
wait(for: [preTxExpectation], timeout: 5)
|
||||
|
||||
let uuids = self.idGenerator.ids
|
||||
|
||||
let expectedStates: [SynchronizerState] = [
|
||||
SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
shieldedBalance: .zero,
|
||||
transparentBalance: .zero,
|
||||
syncStatus: .disconnected,
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: .zero,
|
||||
transparentBalance: .zero,
|
||||
syncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: .zero,
|
||||
transparentBalance: .zero,
|
||||
syncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .enhancing(EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0)),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .enhancing(
|
||||
|
@ -223,6 +234,7 @@ class SychronizerDarksideTests: XCTestCase {
|
|||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .enhancing(
|
||||
|
@ -251,12 +263,14 @@ class SychronizerDarksideTests: XCTestCase {
|
|||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .fetching,
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .synced,
|
||||
|
@ -267,6 +281,202 @@ class SychronizerDarksideTests: XCTestCase {
|
|||
XCTAssertEqual(states, expectedStates)
|
||||
}
|
||||
|
||||
func testSyncSessionUpdates() async throws {
|
||||
var cancellables: [AnyCancellable] = []
|
||||
|
||||
self.idGenerator.ids = [.deadbeef, .beefbeef]
|
||||
|
||||
var states: [SynchronizerState] = []
|
||||
|
||||
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName)
|
||||
let receivedTxHeight: BlockHeight = 663188
|
||||
|
||||
try coordinator.applyStaged(blockheight: receivedTxHeight + 1)
|
||||
|
||||
sleep(2)
|
||||
let preTxExpectation = XCTestExpectation(description: "pre receive")
|
||||
|
||||
coordinator.synchronizer.stateStream
|
||||
.sink { state in
|
||||
states.append(state)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
try await coordinator.sync(
|
||||
completion: { _ in
|
||||
preTxExpectation.fulfill()
|
||||
},
|
||||
error: self.handleError
|
||||
)
|
||||
|
||||
wait(for: [preTxExpectation], timeout: 5)
|
||||
|
||||
let uuids = idGenerator.ids
|
||||
|
||||
let expectedStates: [SynchronizerState] = [
|
||||
SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
shieldedBalance: .zero,
|
||||
transparentBalance: .zero,
|
||||
syncStatus: .disconnected,
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: .zero,
|
||||
transparentBalance: .zero,
|
||||
syncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: .zero,
|
||||
transparentBalance: .zero,
|
||||
syncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .enhancing(EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0)),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .enhancing(
|
||||
EnhancementProgress(
|
||||
totalTransactions: 2,
|
||||
enhancedTransactions: 1,
|
||||
lastFoundTransaction: ZcashTransaction.Overview(
|
||||
blockTime: 1.0,
|
||||
expiryHeight: 663206,
|
||||
fee: Zatoshi(0),
|
||||
id: 2,
|
||||
index: 1,
|
||||
isWalletInternal: true,
|
||||
hasChange: false,
|
||||
memoCount: 1,
|
||||
minedHeight: 663188,
|
||||
raw: Data(),
|
||||
rawID: Data(),
|
||||
receivedNoteCount: 1,
|
||||
sentNoteCount: 0,
|
||||
value: Zatoshi(100000)
|
||||
),
|
||||
range: 663150...663189
|
||||
)
|
||||
),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .enhancing(
|
||||
EnhancementProgress(
|
||||
totalTransactions: 2,
|
||||
enhancedTransactions: 2,
|
||||
lastFoundTransaction: ZcashTransaction.Overview(
|
||||
blockTime: 1.0,
|
||||
expiryHeight: 663192,
|
||||
fee: Zatoshi(0),
|
||||
id: 1,
|
||||
index: 1,
|
||||
isWalletInternal: true,
|
||||
hasChange: false,
|
||||
memoCount: 1,
|
||||
minedHeight: 663174,
|
||||
raw: Data(),
|
||||
rawID: Data(),
|
||||
receivedNoteCount: 1,
|
||||
sentNoteCount: 0,
|
||||
value: Zatoshi(100000)
|
||||
),
|
||||
range: 663150...663189
|
||||
)
|
||||
),
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .fetching,
|
||||
latestScannedHeight: 663150
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .synced,
|
||||
latestScannedHeight: 663189
|
||||
)
|
||||
]
|
||||
|
||||
XCTAssertEqual(states, expectedStates)
|
||||
|
||||
try coordinator.service.applyStaged(nextLatestHeight: 663_200)
|
||||
|
||||
sleep(1)
|
||||
|
||||
states.removeAll()
|
||||
|
||||
let secondSyncExpectation = XCTestExpectation(description: "second sync")
|
||||
|
||||
try await coordinator.sync(
|
||||
completion: { _ in
|
||||
secondSyncExpectation.fulfill()
|
||||
},
|
||||
error: self.handleError
|
||||
)
|
||||
|
||||
wait(for: [secondSyncExpectation], timeout: 5)
|
||||
|
||||
let secondBatchOfExpectedStates: [SynchronizerState] = [
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
|
||||
latestScannedHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .syncing(BlockProgress(startHeight: 663190, targetHeight: 663200, progressHeight: 663200)),
|
||||
latestScannedHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .enhancing(EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0)),
|
||||
latestScannedHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .fetching,
|
||||
latestScannedHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
|
||||
syncStatus: .synced,
|
||||
latestScannedHeight: 663200
|
||||
)
|
||||
]
|
||||
|
||||
XCTAssertEqual(states, secondBatchOfExpectedStates)
|
||||
|
||||
}
|
||||
|
||||
func testSyncAfterWipeWorks() async throws {
|
||||
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName)
|
||||
let receivedTxHeight: BlockHeight = 663188
|
||||
|
@ -340,3 +550,9 @@ extension Zatoshi: CustomDebugStringConvertible {
|
|||
"Zatoshi(\(self.amount))"
|
||||
}
|
||||
}
|
||||
|
||||
extension UUID {
|
||||
static let deadbeef = UUID(uuidString: "DEADBEEF-BEEF-FAFA-BEEF-FAFAFAFAFAFA")!
|
||||
static let beefbeef = UUID(uuidString: "BEEFBEEF-BEEF-DEAD-BEEF-BEEFEBEEFEBE")!
|
||||
static let beefdead = UUID(uuidString: "BEEFDEAD-BEEF-FAFA-DEAD-EAEAEAEAEAEA")!
|
||||
}
|
|
@ -40,6 +40,7 @@ class ClosureSynchronizerOfflineTests: XCTestCase {
|
|||
|
||||
func testStateStreamEmitsAsExpected() {
|
||||
let state = SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
|
||||
syncStatus: .fetching,
|
||||
|
@ -70,6 +71,7 @@ class ClosureSynchronizerOfflineTests: XCTestCase {
|
|||
|
||||
func testLatestStateIsAsExpected() {
|
||||
let state = SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
|
||||
syncStatus: .fetching,
|
||||
|
|
|
@ -38,6 +38,7 @@ class CombineSynchronizerOfflineTests: XCTestCase {
|
|||
|
||||
func testStateStreamEmitsAsExpected() {
|
||||
let state = SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
|
||||
syncStatus: .fetching,
|
||||
|
@ -68,6 +69,7 @@ class CombineSynchronizerOfflineTests: XCTestCase {
|
|||
|
||||
func testLatestStateIsAsExpected() {
|
||||
let state = SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
|
||||
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
|
||||
syncStatus: .fetching,
|
||||
|
|
|
@ -319,4 +319,88 @@ class SynchronizerOfflineTests: XCTestCase {
|
|||
|
||||
wait(for: [expectation], timeout: 1)
|
||||
}
|
||||
|
||||
func testIsNewSessionOnUnpreparedToValidTransition() {
|
||||
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(.nullProgress)))
|
||||
}
|
||||
|
||||
func testIsNotNewSessionOnUnpreparedToStateThatWontSync() {
|
||||
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .error(SynchronizerError.criticalError)))
|
||||
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .disconnected))
|
||||
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .unprepared))
|
||||
}
|
||||
|
||||
func testIsNotNewSessionOnUnpreparedToInvalidOrUnexpectedTransitions() {
|
||||
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .synced))
|
||||
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .fetching))
|
||||
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .enhancing(.zero)))
|
||||
}
|
||||
|
||||
func testIsNotNewSyncSessionOnSameSession() {
|
||||
XCTAssertFalse(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.syncing(
|
||||
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 3)
|
||||
),
|
||||
.syncing(
|
||||
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testIsNewSyncSessionWhenRestartingFromError() {
|
||||
|
||||
XCTAssertTrue(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.error(SynchronizerError.generalError(message: "some error")),
|
||||
.syncing(
|
||||
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testIsNewSyncSessionWhenStartingFromSynced() {
|
||||
XCTAssertTrue(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.synced,
|
||||
.syncing(
|
||||
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testIsNewSyncSessionWhenStartingFromDisconnected() {
|
||||
XCTAssertTrue(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.disconnected,
|
||||
.syncing(
|
||||
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testIsNewSyncSessionWhenStartingFromStopped() {
|
||||
XCTAssertTrue(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.stopped,
|
||||
.syncing(
|
||||
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testSyncStatusesDontDifferWhenOuterStatusIsTheSame() {
|
||||
XCTAssertFalse(SyncStatus.error(SynchronizerError.criticalError).isDifferent(from: .error(.invalidAccount)))
|
||||
XCTAssertFalse(SyncStatus.disconnected.isDifferent(from: .disconnected))
|
||||
XCTAssertFalse(SyncStatus.fetching.isDifferent(from: .fetching))
|
||||
XCTAssertFalse(SyncStatus.stopped.isDifferent(from: .stopped))
|
||||
XCTAssertFalse(SyncStatus.synced.isDifferent(from: .synced))
|
||||
XCTAssertFalse(SyncStatus.syncing(.nullProgress).isDifferent(from: .syncing(.nullProgress)))
|
||||
XCTAssertFalse(SyncStatus.unprepared.isDifferent(from: .unprepared))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Francisco Gindre on 3/31/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import ZcashLightClientKit
|
||||
|
||||
/// This generator will consume the list of UUID passed and fail if empty.
|
||||
class MockSyncSessionIDGenerator: SyncSessionIDGenerator {
|
||||
var ids: [UUID]
|
||||
|
||||
init(ids: [UUID]) {
|
||||
self.ids = ids
|
||||
}
|
||||
|
||||
func nextID() -> UUID {
|
||||
ids.removeFirst()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Francisco Gindre on 3/31/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import ZcashLightClientKit
|
||||
|
||||
extension SDKSynchronizer {
|
||||
convenience init(initializer: Initializer, sessionGenerator: SyncSessionIDGenerator, sessionTicker: SessionTicker) {
|
||||
let metrics = SDKMetrics()
|
||||
self.init(
|
||||
status: .unprepared,
|
||||
initializer: initializer,
|
||||
transactionManager: OutboundTransactionManagerBuilder.build(initializer: initializer),
|
||||
transactionRepository: initializer.transactionRepository,
|
||||
utxoRepository: UTXORepositoryBuilder.build(initializer: initializer),
|
||||
blockProcessor: CompactBlockProcessor(
|
||||
initializer: initializer,
|
||||
metrics: metrics,
|
||||
logger: initializer.logger,
|
||||
walletBirthdayProvider: { initializer.walletBirthday }
|
||||
),
|
||||
metrics: metrics,
|
||||
syncSessionIDGenerator: sessionGenerator,
|
||||
syncSessionTicker: sessionTicker
|
||||
)
|
||||
}
|
||||
}
|
|
@ -58,7 +58,8 @@ class TestCoordinator {
|
|||
walletBirthday: BlockHeight,
|
||||
network: ZcashNetwork,
|
||||
callPrepareInConstructor: Bool = true,
|
||||
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint
|
||||
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint,
|
||||
syncSessionIDGenerator: SyncSessionIDGenerator = UniqueSyncSessionIDGenerator()
|
||||
) async throws {
|
||||
let derivationTool = DerivationTool(networkType: network.networkType)
|
||||
|
||||
|
@ -76,7 +77,8 @@ class TestCoordinator {
|
|||
walletBirthday: walletBirthday,
|
||||
network: network,
|
||||
callPrepareInConstructor: callPrepareInConstructor,
|
||||
endpoint: endpoint
|
||||
endpoint: endpoint,
|
||||
syncSessionIDGenerator: syncSessionIDGenerator
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -87,7 +89,8 @@ class TestCoordinator {
|
|||
walletBirthday: BlockHeight,
|
||||
network: ZcashNetwork,
|
||||
callPrepareInConstructor: Bool = true,
|
||||
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint
|
||||
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint,
|
||||
syncSessionIDGenerator: SyncSessionIDGenerator
|
||||
) async throws {
|
||||
await InternalSyncProgress(alias: alias, storage: UserDefaults.standard, logger: logger).rewind(to: 0)
|
||||
|
||||
|
@ -114,7 +117,7 @@ class TestCoordinator {
|
|||
logLevel: .debug
|
||||
)
|
||||
|
||||
let synchronizer = SDKSynchronizer(initializer: initializer)
|
||||
let synchronizer = SDKSynchronizer(initializer: initializer, sessionGenerator: syncSessionIDGenerator, sessionTicker: .live)
|
||||
|
||||
self.synchronizer = synchronizer
|
||||
subscribeToState(synchronizer: self.synchronizer)
|
||||
|
|
Loading…
Reference in New Issue