Merge branch 'master' into merge_master_to_zip_316

This commit is contained in:
Francisco Gindre 2022-09-15 11:04:49 -03:00
commit bee1c9db80
32 changed files with 1211 additions and 2000 deletions

View File

@ -31,7 +31,7 @@ class SyncBlocksViewController: UIViewController {
// swiftlint:disable:next force_try // swiftlint:disable:next force_try
_ = try! wallet.initialize(with: DemoAppConfig.seed) _ = try! wallet.initialize(with: DemoAppConfig.seed)
processor = CompactBlockProcessor(initializer: wallet) processor = CompactBlockProcessor(initializer: wallet)
statusLabel.text = textFor(state: processor?.state ?? .stopped) statusLabel.text = textFor(state: processor?.state.getState() ?? .stopped)
progressBar.progress = 0 progressBar.progress = 0
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
@ -70,7 +70,7 @@ class SyncBlocksViewController: UIViewController {
@IBAction func startStop() { @IBAction func startStop() {
guard let processor = processor else { return } guard let processor = processor else { return }
switch processor.state { switch processor.state.getState() {
case .stopped: case .stopped:
startProcessor() startProcessor()
default: default:
@ -92,7 +92,7 @@ class SyncBlocksViewController: UIViewController {
func stopProcessor() { func stopProcessor() {
guard let processor = processor else { return } guard let processor = processor else { return }
processor.stop(cancelTasks: true) processor.stop()
updateUI() updateUI()
} }
@ -114,7 +114,7 @@ class SyncBlocksViewController: UIViewController {
} }
func updateUI() { func updateUI() {
guard let state = processor?.state else { return } guard let state = processor?.state.getState() else { return }
statusLabel.text = textFor(state: state) statusLabel.text = textFor(state: state)
startPause.setTitle(buttonText(for: state), for: .normal) startPause.setTitle(buttonText(for: state), for: .normal)

View File

@ -0,0 +1,160 @@
//
// CompactBlockDownload.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 10/16/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
extension CompactBlockProcessor {
func compactBlockStreamDownload(
blockBufferSize: Int,
startHeight: BlockHeight? = nil,
targetHeight: BlockHeight? = nil
) async throws {
try Task.checkCancellation()
setState(.downloading)
var buffer: [ZcashCompactBlock] = []
var targetHeightInternal: BlockHeight?
do {
targetHeightInternal = targetHeight
if targetHeight == nil {
targetHeightInternal = try await service.latestBlockHeightAsync()
}
guard let latestHeight = targetHeightInternal else {
throw LightWalletServiceError.generalError(message: "missing target height on compactBlockStreamDownload")
}
let latestDownloaded = try await storage.latestHeightAsync()
let startHeight = max(startHeight ?? BlockHeight.empty(), latestDownloaded)
let stream = service.blockStream(
startHeight: startHeight,
endHeight: latestHeight
)
for try await zcashCompactBlock in stream {
buffer.append(zcashCompactBlock)
if buffer.count >= blockBufferSize {
// TODO: writeAsync doesn't make sense here, awaiting it or calling blocking API have the same result and impact
try storage.write(blocks: buffer)
buffer.removeAll(keepingCapacity: true)
}
let progress = BlockProgress(
startHeight: startHeight,
targetHeight: latestHeight,
progressHeight: zcashCompactBlock.height
)
notifyProgress(.download(progress))
}
// TODO: writeAsync doesn't make sense here, awaiting it or calling blocking API have the same result and impact
try storage.write(blocks: buffer)
buffer.removeAll(keepingCapacity: true)
} catch {
guard let err = error as? LightWalletServiceError, case .userCancelled = err else {
throw error
}
}
}
}
extension CompactBlockProcessor {
func compactBlockDownload(
downloader: CompactBlockDownloading,
range: CompactBlockRange
) async throws {
try Task.checkCancellation()
do {
try await downloader.downloadBlockRangeAsync(range)
} catch {
throw error
}
}
}
extension CompactBlockProcessor {
enum CompactBlockBatchDownloadError: Error {
case startHeightMissing
case batchDownloadFailed(range: CompactBlockRange, error: Error?)
}
func compactBlockBatchDownload(
range: CompactBlockRange,
batchSize: Int = 100,
maxRetries: Int = 5
) async throws {
try Task.checkCancellation()
var startHeight = range.lowerBound
let targetHeight = range.upperBound
do {
let localDownloadedHeight = try await self.storage.latestHeightAsync()
if localDownloadedHeight != BlockHeight.empty() && localDownloadedHeight > startHeight {
LoggerProxy.warn("provided startHeight (\(startHeight)) differs from local latest downloaded height (\(localDownloadedHeight))")
startHeight = localDownloadedHeight + 1
}
var currentHeight = startHeight
notifyProgress(
.download(
BlockProgress(
startHeight: currentHeight,
targetHeight: targetHeight,
progressHeight: currentHeight
)
)
)
while !Task.isCancelled && currentHeight <= targetHeight {
var retries = 0
var success = true
var localError: Error?
let range = CompactBlockRange(uncheckedBounds: (lower: currentHeight, upper: min(currentHeight + batchSize, targetHeight)))
repeat {
do {
let stream: AsyncThrowingStream<ZcashCompactBlock, Error> = service.blockRange(range)
var blocks: [ZcashCompactBlock] = []
for try await compactBlock in stream {
blocks.append(compactBlock)
}
try storage.insert(blocks)
success = true
} catch {
success = false
localError = error
retries += 1
}
} while !Task.isCancelled && !success && retries < maxRetries
if retries >= maxRetries {
throw CompactBlockBatchDownloadError.batchDownloadFailed(range: range, error: localError)
}
notifyProgress(
.download(
BlockProgress(
startHeight: startHeight,
targetHeight: targetHeight,
progressHeight: range.upperBound
)
)
)
currentHeight = range.upperBound + 1
}
} catch {
throw error
}
}
}

View File

@ -1,318 +0,0 @@
//
// CompactBlockDownloadOperation.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 10/16/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
class CompactBlockDownloadOperation: ZcashOperation {
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
private var downloader: CompactBlockDownloading
private var range: CompactBlockRange
private var cancelableTask: Task<Void, Error>?
private var done = false
required init(downloader: CompactBlockDownloading, range: CompactBlockRange) {
self.range = range
self.downloader = downloader
super.init()
self.name = "Download Operation: \(range)"
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
cancelableTask = Task {
do {
try await downloader.downloadBlockRangeAsync(range)
self.done = true
} catch {
self.fail(error: error)
}
}
while !done && !isCancelled {
sleep(1)
}
}
override func fail(error: Error? = nil) {
self.cancelableTask?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelableTask?.cancel()
super.cancel()
}
}
protocol CompactBlockProgressDelegate: AnyObject {
func progressUpdated(_ progress: CompactBlockProgress)
}
class CompactBlockStreamDownloadOperation: ZcashOperation {
enum CompactBlockStreamDownloadOperationError: Error {
case startHeightMissing
}
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
private var storage: CompactBlockStorage
private var service: LightWalletService
private var done = false
private var cancelableTask: Task<Void, Error>?
private var startHeight: BlockHeight?
private var targetHeight: BlockHeight?
private var blockBufferSize: Int
private var buffer: [ZcashCompactBlock] = []
private weak var progressDelegate: CompactBlockProgressDelegate?
/// Creates an Compact Block Stream Download Operation Operation
/// - Parameters:
/// - service: instance that conforms to `LightWalletService`
/// - storage: instance that conforms to `CompactBlockStorage`
/// - blockBufferSize: the number of blocks that the stream downloader will store in memory
/// before writing them to disk. Making this number smaller makes the downloader easier on RAM
/// memory while being less efficient on disk writes. Making it bigger takes up more RAM memory
/// but is less straining on Disk Writes. Too little or too big buffer will make this less efficient.
/// - startHeight: the height this downloader will start downloading from. If `nil`,
/// it will start from the latest height found on the local cacheDb
/// - targetHeight: the upper bound for this stream download. If `nil`, the
/// streamer will call `service.latestBlockHeight()`
/// - progressDelegate: Optional delegate to report ongoing progress conforming to
/// `CompactBlockProgressDelegate`
///
required init(
service: LightWalletService,
storage: CompactBlockStorage,
blockBufferSize: Int,
startHeight: BlockHeight? = nil,
targetHeight: BlockHeight? = nil,
progressDelegate: CompactBlockProgressDelegate? = nil
) {
self.storage = storage
self.service = service
self.startHeight = startHeight
self.targetHeight = targetHeight
self.progressDelegate = progressDelegate
self.blockBufferSize = blockBufferSize
super.init()
self.name = "Download Stream Operation"
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
cancelableTask = Task {
do {
if self.targetHeight == nil {
self.targetHeight = try await service.latestBlockHeightAsync()
}
guard let latestHeight = self.targetHeight else {
throw LightWalletServiceError.generalError(message: "missing target height on block stream operation")
}
let latestDownloaded = try await storage.latestHeightAsync()
let startHeight = max(self.startHeight ?? BlockHeight.empty(), latestDownloaded)
let stream = service.blockStream(
startHeight: startHeight,
endHeight: latestHeight
)
for try await zcashCompactBlock in stream {
try self.cache(zcashCompactBlock, flushCache: false)
let progress = BlockProgress(
startHeight: startHeight,
targetHeight: latestHeight,
progressHeight: zcashCompactBlock.height
)
self.progressDelegate?.progressUpdated(.download(progress))
}
try self.flush()
self.done = true
} catch {
if let err = error as? LightWalletServiceError, case .userCancelled = err {
self.done = true
} else {
self.fail(error: error)
}
}
}
while !done && !isCancelled {
sleep(1)
}
}
override func fail(error: Error? = nil) {
self.cancelableTask?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelableTask?.cancel()
super.cancel()
}
func cache(_ block: ZcashCompactBlock, flushCache: Bool) throws {
self.buffer.append(block)
if flushCache || buffer.count >= blockBufferSize {
try flush()
}
}
func flush() throws {
try self.storage.write(blocks: self.buffer)
self.buffer.removeAll(keepingCapacity: true)
}
}
class CompactBlockBatchDownloadOperation: ZcashOperation {
enum CompactBlockBatchDownloadOperationError: Error {
case startHeightMissing
case batchDownloadFailed(range: CompactBlockRange, error: Error?)
}
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
private var batch: Int
private var done = false
private var maxRetries: Int
private var storage: CompactBlockStorage
private var service: LightWalletService
private var cancelableTask: Task<Void, Error>?
private var startHeight: BlockHeight
private var targetHeight: BlockHeight
private weak var progressDelegate: CompactBlockProgressDelegate?
required init(
service: LightWalletService,
storage: CompactBlockStorage,
startHeight: BlockHeight,
targetHeight: BlockHeight,
batchSize: Int = 100,
maxRetries: Int = 5,
progressDelegate: CompactBlockProgressDelegate? = nil
) {
self.storage = storage
self.service = service
self.startHeight = startHeight
self.targetHeight = targetHeight
self.progressDelegate = progressDelegate
self.batch = batchSize
self.maxRetries = maxRetries
super.init()
self.name = "Download Batch Operation"
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
cancelableTask = Task {
do {
let localDownloadedHeight = try await self.storage.latestHeightAsync()
if localDownloadedHeight != BlockHeight.empty() && localDownloadedHeight > startHeight {
LoggerProxy.warn("provided startHeight (\(startHeight)) differs from local latest downloaded height (\(localDownloadedHeight))")
startHeight = localDownloadedHeight + 1
}
var currentHeight = startHeight
self.progressDelegate?.progressUpdated(
.download(
BlockProgress(
startHeight: currentHeight,
targetHeight: targetHeight,
progressHeight: currentHeight
)
)
)
while !isCancelled && currentHeight <= targetHeight {
var retries = 0
var success = true
var localError: Error?
let range = nextRange(currentHeight: currentHeight, targetHeight: targetHeight)
repeat {
do {
let stream: AsyncThrowingStream<ZcashCompactBlock, Error> = service.blockRange(range)
var blocks: [ZcashCompactBlock] = []
for try await compactBlock in stream {
blocks.append(compactBlock)
}
try storage.insert(blocks)
success = true
} catch {
success = false
localError = error
retries += 1
}
} while !isCancelled && !success && retries < maxRetries
if retries >= maxRetries {
throw CompactBlockBatchDownloadOperationError.batchDownloadFailed(range: range, error: localError)
}
self.progressDelegate?.progressUpdated(
.download(
BlockProgress(
startHeight: startHeight,
targetHeight: targetHeight,
progressHeight: range.upperBound
)
)
)
currentHeight = range.upperBound + 1
}
self.done = true
} catch {
self.fail(error: error)
}
}
while !done && !isCancelled {
sleep(1)
}
}
override func fail(error: Error? = nil) {
self.cancelableTask?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelableTask?.cancel()
super.cancel()
}
func nextRange(currentHeight: BlockHeight, targetHeight: BlockHeight) -> CompactBlockRange {
CompactBlockRange(uncheckedBounds: (lower: currentHeight, upper: min(currentHeight + batch, targetHeight)))
}
}

View File

@ -0,0 +1,119 @@
//
// CompactBlockEnhancement.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 4/10/20.
//
import Foundation
extension CompactBlockProcessor {
enum EnhancementError: Error {
case noRawData(message: String)
case unknownError
case decryptError(error: Error)
case txIdNotFound(txId: Data)
}
private func enhance(transaction: TransactionEntity) async throws -> ConfirmedTransactionEntity {
LoggerProxy.debug("Zoom.... Enhance... Tx: \(transaction.transactionId.toHexStringTxId())")
let transaction = try await downloader.fetchTransactionAsync(txId: transaction.transactionId)
let transactionID = transaction.transactionId.toHexStringTxId()
let block = String(describing: transaction.minedHeight)
LoggerProxy.debug("Decrypting and storing transaction id: \(transactionID) block: \(block)")
guard let rawBytes = transaction.raw?.bytes else {
let error = EnhancementError.noRawData(
message: "Critical Error: transaction id: \(transaction.transactionId.toHexStringTxId()) has no data"
)
LoggerProxy.error("\(error)")
throw error
}
guard let minedHeight = transaction.minedHeight else {
let error = EnhancementError.noRawData(
message: "Critical Error - Attempt to decrypt and store an unmined transaction. Id: \(transaction.transactionId.toHexStringTxId())"
)
LoggerProxy.error("\(error)")
throw error
}
guard rustBackend.decryptAndStoreTransaction(dbData: config.dataDb, txBytes: rawBytes, minedHeight: Int32(minedHeight), networkType: config.network.networkType) else {
if let rustError = rustBackend.lastError() {
throw EnhancementError.decryptError(error: rustError)
}
throw EnhancementError.unknownError
}
guard let confirmedTx = try transactionRepository.findConfirmedTransactionBy(rawId: transaction.transactionId) else {
throw EnhancementError.txIdNotFound(txId: transaction.transactionId)
}
return confirmedTx
}
func compactBlockEnhancement(range: CompactBlockRange) async throws {
try Task.checkCancellation()
LoggerProxy.debug("Started Enhancing range: \(range)")
setState(.enhancing)
let blockRange = range.blockRange()
var retries = 0
let maxRetries = 5
// fetch transactions
do {
guard let transactions = try transactionRepository.findTransactions(in: blockRange, limit: Int.max), !transactions.isEmpty else {
LoggerProxy.debug("no transactions detected on range: \(blockRange.printRange)")
return
}
for index in 0 ..< transactions.count {
let transaction = transactions[index]
var retry = true
while retry && retries < maxRetries {
try Task.checkCancellation()
do {
let confirmedTx = try await enhance(transaction: transaction)
retry = false
notifyProgress(
.enhance(
EnhancementStreamProgress(
totalTransactions: transactions.count,
enhancedTransactions: index + 1,
lastFoundTransaction: confirmedTx,
range: range
)
)
)
} catch {
retries += 1
LoggerProxy.error("could not enhance txId \(transaction.transactionId.toHexStringTxId()) - Error: \(error)")
if retries > maxRetries {
throw error
}
}
}
}
} catch {
LoggerProxy.error("error enhancing transactions! \(error)")
throw error
}
if let foundTxs = try? transactionRepository.findConfirmedTransactions(in: blockRange, offset: 0, limit: Int.max) {
notifyTransactions(foundTxs, in: blockRange)
}
if Task.isCancelled {
LoggerProxy.debug("Warning: compactBlockEnhancement on range \(range) cancelled")
}
}
}
private extension BlockRange {
var printRange: String {
"\(self.start.height) ... \(self.end.height)"
}
}

View File

@ -1,175 +0,0 @@
//
// CompactBlockEnhancementOperation.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 4/10/20.
//
import Foundation
class CompactBlockEnhancementOperation: ZcashOperation {
enum EnhancementError: Error {
case noRawData(message: String)
case unknownError
case decryptError(error: Error)
case txIdNotFound(txId: Data)
}
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
var rustBackend: ZcashRustBackendWelding.Type
var txFoundHandler: (([ConfirmedTransactionEntity], BlockRange) -> Void)?
var downloader: CompactBlockDownloading
var repository: TransactionRepository
var range: BlockRange
var maxRetries: Int = 5
var retries: Int = 0
private(set) var network: NetworkType
private weak var progressDelegate: CompactBlockProgressDelegate?
private var dataDb: URL
private var cancelableTask: Task<Void, Error>?
private var done = false
init(
rustWelding: ZcashRustBackendWelding.Type,
dataDb: URL,
downloader: CompactBlockDownloading,
repository: TransactionRepository,
range: BlockRange,
networkType: NetworkType,
progressDelegate: CompactBlockProgressDelegate? = nil
) {
rustBackend = rustWelding
self.dataDb = dataDb
self.downloader = downloader
self.repository = repository
self.range = range
self.progressDelegate = progressDelegate
self.network = networkType
super.init()
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
cancelableTask = Task {
// fetch transactions
do {
guard let transactions = try repository.findTransactions(in: self.range, limit: Int.max), !transactions.isEmpty else {
LoggerProxy.debug("no transactions detected on range: \(range.printRange)")
return
}
for index in 0 ..< transactions.count {
let transaction = transactions[index]
var retry = true
while retry && self.retries < maxRetries {
do {
let confirmedTx = try await enhance(transaction: transaction)
retry = false
self.reportProgress(
totalTransactions: transactions.count,
enhanced: index + 1,
txEnhanced: confirmedTx
)
} catch {
self.retries += 1
LoggerProxy.error("could not enhance txId \(transaction.transactionId.toHexStringTxId()) - Error: \(error)")
if retries > maxRetries {
throw error
}
}
}
}
} catch {
LoggerProxy.error("error enhancing transactions! \(error)")
self.fail(error: error)
return
}
if let handler = self.txFoundHandler, let foundTxs = try? repository.findConfirmedTransactions(in: self.range, offset: 0, limit: Int.max) {
handler(foundTxs, self.range)
}
self.done = true
}
while !done && !isCancelled {
sleep(1)
}
}
func reportProgress(totalTransactions: Int, enhanced: Int, txEnhanced: ConfirmedTransactionEntity) {
self.progressDelegate?.progressUpdated(
.enhance(
EnhancementStreamProgress(
totalTransactions: totalTransactions,
enhancedTransactions: enhanced,
lastFoundTransaction: txEnhanced,
range: self.range.compactBlockRange
)
)
)
}
func enhance(transaction: TransactionEntity) async throws -> ConfirmedTransactionEntity {
LoggerProxy.debug("Zoom.... Enhance... Tx: \(transaction.transactionId.toHexStringTxId())")
let transaction = try await downloader.fetchTransactionAsync(txId: transaction.transactionId)
let transactionID = transaction.transactionId.toHexStringTxId()
let block = String(describing: transaction.minedHeight)
LoggerProxy.debug("Decrypting and storing transaction id: \(transactionID) block: \(block)")
guard let rawBytes = transaction.raw?.bytes else {
let error = EnhancementError.noRawData(
message: "Critical Error: transaction id: \(transaction.transactionId.toHexStringTxId()) has no data"
)
LoggerProxy.error("\(error)")
throw error
}
guard let minedHeight = transaction.minedHeight else {
let error = EnhancementError.noRawData(
message: "Critical Error - Attempt to decrypt and store an unmined transaction. Id: \(transaction.transactionId.toHexStringTxId())"
)
LoggerProxy.error("\(error)")
throw error
}
guard rustBackend.decryptAndStoreTransaction(dbData: dataDb, txBytes: rawBytes, minedHeight: Int32(minedHeight), networkType: network) else {
if let rustError = rustBackend.lastError() {
throw EnhancementError.decryptError(error: rustError)
}
throw EnhancementError.unknownError
}
guard let confirmedTx = try self.repository.findConfirmedTransactionBy(rawId: transaction.transactionId) else {
throw EnhancementError.txIdNotFound(txId: transaction.transactionId)
}
return confirmedTx
}
override func fail(error: Error? = nil) {
self.cancelableTask?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelableTask?.cancel()
super.cancel()
}
}
private extension BlockRange {
var printRange: String {
"\(self.start.height) ... \(self.end.height)"
}
}

View File

@ -30,6 +30,7 @@ public enum CompactBlockProcessorError: Error {
case wrongConsensusBranchId(expectedLocally: ConsensusBranchID, found: ConsensusBranchID) case wrongConsensusBranchId(expectedLocally: ConsensusBranchID, found: ConsensusBranchID)
case networkMismatch(expected: NetworkType, found: NetworkType) case networkMismatch(expected: NetworkType, found: NetworkType)
case saplingActivationMismatch(expected: BlockHeight, found: BlockHeight) case saplingActivationMismatch(expected: BlockHeight, found: BlockHeight)
case unknown
} }
/** /**
@ -311,11 +312,30 @@ public class CompactBlockProcessor {
case synced case synced
} }
public private(set) var state: State = .stopped { // TODO: this isn't an Actor even though it looks like a good candidate, the reason:
didSet { // `state` lives in both sync and async environments. An Actor is demanding async context only
transitionState(from: oldValue, to: self.state) // so we can't take the advantage unless we encapsulate all `state` reads/writes to async context.
// Therefore solution with class + lock works for us butr eventually will be replaced.
// The future of CompactBlockProcessor is an actor (we won't need to encapsulate the state separately), issue 523,
// https://github.com/zcash/ZcashLightClientKit/issues/523
public class ThreadSafeState {
private var state: State = .stopped
let lock = NSLock()
func setState(_ newState: State) {
lock.lock()
defer { lock.unlock() }
state = newState
}
public func getState() -> State {
lock.lock()
defer { lock.unlock() }
return state
} }
} }
public internal(set) var state = ThreadSafeState()
var config: Configuration { var config: Configuration {
willSet { willSet {
@ -328,7 +348,7 @@ public class CompactBlockProcessor {
} }
var shouldStart: Bool { var shouldStart: Bool {
switch self.state { switch self.state.getState() {
case .stopped, .synced, .error: case .stopped, .synced, .error:
return !maxAttemptsReached return !maxAttemptsReached
default: default:
@ -336,19 +356,19 @@ public class CompactBlockProcessor {
} }
} }
private var service: LightWalletService var service: LightWalletService
private(set) var downloader: CompactBlockDownloading private(set) var downloader: CompactBlockDownloading
private var storage: CompactBlockStorage var storage: CompactBlockStorage
private var transactionRepository: TransactionRepository var transactionRepository: TransactionRepository
private var accountRepository: AccountRepository var accountRepository: AccountRepository
private var rustBackend: ZcashRustBackendWelding.Type var rustBackend: ZcashRustBackendWelding.Type
private var retryAttempts: Int = 0 private var retryAttempts: Int = 0
private var backoffTimer: Timer? private var backoffTimer: Timer?
private var lowerBoundHeight: BlockHeight? private var lowerBoundHeight: BlockHeight?
private var latestBlockHeight: BlockHeight private var latestBlockHeight: BlockHeight
private var lastChainValidationFailure: BlockHeight? private var lastChainValidationFailure: BlockHeight?
private var consecutiveChainValidationErrors: Int = 0 private var consecutiveChainValidationErrors: Int = 0
private var processingError: Error? var processingError: Error?
private var foundBlocks = false private var foundBlocks = false
private var maxAttempts: Int { private var maxAttempts: Int {
config.retries config.retries
@ -358,13 +378,7 @@ public class CompactBlockProcessor {
BlockHeight(self.config.downloadBatchSize) BlockHeight(self.config.downloadBatchSize)
} }
private var operationQueue: OperationQueue = { private var cancelableTask: Task<Void, Error>?
let queue = OperationQueue()
queue.name = "CompactBlockProcessorQueue"
queue.maxConcurrentOperationCount = 1
return queue
}()
/// Initializes a CompactBlockProcessor instance /// Initializes a CompactBlockProcessor instance
/// - Parameters: /// - Parameters:
@ -431,7 +445,13 @@ public class CompactBlockProcessor {
} }
deinit { deinit {
self.operationQueue.cancelAllOperations() cancelableTask?.cancel()
}
func setState(_ newState: State) {
let oldValue = state.getState()
state.setState(newState)
transitionState(from: oldValue, to: newState)
} }
static func validateServerInfo( static func validateServerInfo(
@ -487,19 +507,14 @@ public class CompactBlockProcessor {
self.backoffTimer?.invalidate() self.backoffTimer?.invalidate()
self.backoffTimer = nil self.backoffTimer = nil
} }
guard !operationQueue.isSuspended else {
LoggerProxy.debug("restarting suspended queue")
operationQueue.isSuspended = false
return
}
guard shouldStart else { guard shouldStart else {
switch self.state { switch self.state.getState() {
case .error(let e): case .error(let e):
// max attempts have been reached // max attempts have been reached
LoggerProxy.info("max retry attempts reached with error: \(e)") LoggerProxy.info("max retry attempts reached with error: \(e)")
notifyError(CompactBlockProcessorError.maxAttemptsReached(attempts: self.maxAttempts)) notifyError(CompactBlockProcessorError.maxAttemptsReached(attempts: self.maxAttempts))
self.state = .stopped setState(.stopped)
case .stopped: case .stopped:
// max attempts have been reached // max attempts have been reached
LoggerProxy.info("max retry attempts reached") LoggerProxy.info("max retry attempts reached")
@ -514,7 +529,7 @@ public class CompactBlockProcessor {
return return
} }
self.nextBatchTask() self.nextBatch()
} }
/** /**
@ -523,18 +538,14 @@ public class CompactBlockProcessor {
Note: retry count is reset Note: retry count is reset
- Parameter cancelTasks: cancel the pending tasks. Defaults to true - Parameter cancelTasks: cancel the pending tasks. Defaults to true
*/ */
public func stop(cancelTasks: Bool = true) { public func stop() {
self.backoffTimer?.invalidate() self.backoffTimer?.invalidate()
self.backoffTimer = nil self.backoffTimer = nil
if cancelTasks { cancelableTask?.cancel()
operationQueue.cancelAllOperations()
} else {
self.operationQueue.isSuspended = true
}
self.retryAttempts = 0 self.retryAttempts = 0
self.state = .stopped setState(.stopped)
} }
/** /**
@ -577,7 +588,7 @@ public class CompactBlockProcessor {
- Throws CompactBlockProcessorError.invalidConfiguration if block height is invalid or if processor is already started - Throws CompactBlockProcessorError.invalidConfiguration if block height is invalid or if processor is already started
*/ */
func setStartHeight(_ startHeight: BlockHeight) throws { func setStartHeight(_ startHeight: BlockHeight) throws {
guard self.state == .stopped, startHeight >= config.network.constants.saplingActivationHeight else { guard self.state.getState() == .stopped, startHeight >= config.network.constants.saplingActivationHeight else {
throw CompactBlockProcessorError.invalidConfiguration throw CompactBlockProcessorError.invalidConfiguration
} }
@ -612,243 +623,34 @@ public class CompactBlockProcessor {
}) })
} }
/**
processes new blocks on the given range based on the configuration set for this instance /// Processes new blocks on the given range based on the configuration set for this instance
the way operations are queued is implemented based on the following good practice https://forums.developer.apple.com/thread/25761
*/
// swiftlint:disable cyclomatic_complexity
func processNewBlocks(range: CompactBlockRange) { func processNewBlocks(range: CompactBlockRange) {
self.foundBlocks = true self.foundBlocks = true
self.backoffTimer?.invalidate() self.backoffTimer?.invalidate()
self.backoffTimer = nil self.backoffTimer = nil
let cfg = self.config cancelableTask = Task(priority: .userInitiated) {
let downloadBlockOperation = CompactBlockStreamDownloadOperation( do {
service: self.service, try await compactBlockStreamDownload(
storage: self.storage, blockBufferSize: config.downloadBufferSize,
blockBufferSize: self.config.downloadBufferSize, startHeight: range.lowerBound,
startHeight: range.lowerBound, targetHeight: range.upperBound
targetHeight: range.upperBound, )
progressDelegate: self try await compactBlockValidation()
) try await compactBlockBatchScanning(range: range)
try await compactBlockEnhancement(range: range)
downloadBlockOperation.startedHandler = { [weak self] in try await fetchUnspentTxOutputs(range: range)
DispatchQueue.main.async { } catch {
self?.state = .downloading if error is CancellationError {
}
}
downloadBlockOperation.errorHandler = { [weak self] error in
DispatchQueue.main.async {
guard let self = self else { return }
self.processingError = error
self.fail(error)
}
}
let validateChainOperation = CompactBlockValidationOperation(
rustWelding: self.rustBackend,
cacheDb: cfg.cacheDb,
dataDb: cfg.dataDb,
networkType: self.config.network.networkType
)
let downloadValidateAdapterOperation = BlockOperation { [weak validateChainOperation, weak downloadBlockOperation] in
validateChainOperation?.error = downloadBlockOperation?.error
}
validateChainOperation.completionHandler = { [weak self] _, cancelled in
guard !cancelled else {
DispatchQueue.main.async {
self?.state = .stopped
LoggerProxy.debug("Warning: validateChainOperation operation cancelled")
}
return
}
LoggerProxy.debug("validateChainFinished")
}
validateChainOperation.errorHandler = { [weak self] error in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard let validationError = error as? CompactBlockValidationError else {
LoggerProxy.error("Warning: validateChain operation returning generic error: \(error)")
return
}
switch validationError {
case .validationFailed(let height):
LoggerProxy.debug("chain validation at height: \(height)")
self.validationFailed(at: height)
case .failedWithError(let e):
guard let validationFailure = e else {
LoggerProxy.error("validation failed without a specific error")
self.fail(CompactBlockProcessorError.generalError(message: "validation failed without a specific error"))
return
}
self.fail(validationFailure) }
if !(Task.isCancelled) {
fail(error)
} }
} }
} }
validateChainOperation.startedHandler = { [weak self] in
DispatchQueue.main.async { [weak self] in
self?.state = .validating
}
}
let scanBlocksOperation = CompactBlockBatchScanningOperation(
rustWelding: rustBackend,
cacheDb: config.cacheDb,
dataDb: config.dataDb,
transactionRepository: transactionRepository,
range: range,
batchSize: UInt32(self.config.scanningBatchSize),
networkType: self.config.network.networkType,
progressDelegate: self
)
let validateScanningAdapterOperation = BlockOperation { [weak scanBlocksOperation, weak validateChainOperation] in
scanBlocksOperation?.error = validateChainOperation?.error
}
scanBlocksOperation.startedHandler = { [weak self] in
DispatchQueue.main.async { [weak self] in
self?.state = .scanning
}
}
scanBlocksOperation.completionHandler = { [weak self] _, cancelled in
guard !cancelled else {
DispatchQueue.main.async { [weak self] in
self?.state = .stopped
LoggerProxy.debug("Warning: scanBlocksOperation operation cancelled")
}
return
}
}
scanBlocksOperation.errorHandler = { [weak self] error in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.processingError = error
self.fail(error)
}
}
let enhanceOperation = CompactBlockEnhancementOperation(
rustWelding: rustBackend,
dataDb: config.dataDb,
downloader: downloader,
repository: transactionRepository,
range: range.blockRange(),
networkType: self.config.network.networkType
)
enhanceOperation.startedHandler = {
LoggerProxy.debug("Started Enhancing range: \(range)")
DispatchQueue.main.async { [weak self] in
self?.state = .enhancing
}
}
enhanceOperation.txFoundHandler = { [weak self] txs, range in
self?.notifyTransactions(txs, in: range)
}
enhanceOperation.completionHandler = { _, cancelled in
guard !cancelled else {
LoggerProxy.debug("Warning: enhance operation on range \(range) cancelled")
return
}
}
enhanceOperation.errorHandler = { [weak self] error in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.processingError = error
self.fail(error)
}
}
let scanEnhanceAdapterOperation = BlockOperation { [weak enhanceOperation, weak scanBlocksOperation] in
enhanceOperation?.error = scanBlocksOperation?.error
}
let fetchOperation = FetchUnspentTxOutputsOperation(
accountRepository: accountRepository,
downloader: self.downloader,
rustbackend: rustBackend,
dataDb: config.dataDb,
startHeight: config.walletBirthday,
networkType: self.config.network.networkType
)
fetchOperation.startedHandler = { [weak self] in
DispatchQueue.main.async { [weak self] in
self?.state = .fetching
}
}
fetchOperation.completionHandler = { [weak self] _, cancelled in
guard !cancelled else {
LoggerProxy.debug("Warning: fetch operation on range \(range) cancelled")
return
}
DispatchQueue.main.async { [weak self] in
self?.processBatchFinished(range: range)
}
}
fetchOperation.errorHandler = { [weak self] error in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.processingError = error
self.fail(error)
}
}
fetchOperation.fetchedUTXOsHandler = { result in
NotificationCenter.default.post(
name: .blockProcessorStoredUTXOs,
object: self,
userInfo: [CompactBlockProcessorNotificationKey.refreshedUTXOs: result]
)
}
let enhanceFetchAdapterOperation = BlockOperation { [weak fetchOperation, weak enhanceOperation] in
fetchOperation?.error = enhanceOperation?.error
}
downloadValidateAdapterOperation.addDependency(downloadBlockOperation)
validateChainOperation.addDependency(downloadValidateAdapterOperation)
validateScanningAdapterOperation.addDependency(validateChainOperation)
scanBlocksOperation.addDependency(validateScanningAdapterOperation)
scanEnhanceAdapterOperation.addDependency(scanBlocksOperation)
enhanceOperation.addDependency(scanEnhanceAdapterOperation)
enhanceFetchAdapterOperation.addDependency(enhanceOperation)
fetchOperation.addDependency(enhanceFetchAdapterOperation)
operationQueue.addOperations(
[
downloadBlockOperation,
downloadValidateAdapterOperation,
validateChainOperation,
validateScanningAdapterOperation,
scanBlocksOperation,
scanEnhanceAdapterOperation,
enhanceOperation,
enhanceFetchAdapterOperation,
fetchOperation
],
waitUntilFinished: false
)
} }
func calculateProgress(start: BlockHeight, current: BlockHeight, latest: BlockHeight) -> Float { func calculateProgress(start: BlockHeight, current: BlockHeight, latest: BlockHeight) -> Float {
@ -892,35 +694,35 @@ public class CompactBlockProcessor {
} }
func severeFailure(_ error: Error) { func severeFailure(_ error: Error) {
operationQueue.cancelAllOperations() cancelableTask?.cancel()
LoggerProxy.error("show stoppper failure: \(error)") LoggerProxy.error("show stoppper failure: \(error)")
self.backoffTimer?.invalidate() self.backoffTimer?.invalidate()
self.retryAttempts = config.retries self.retryAttempts = config.retries
self.processingError = error self.processingError = error
self.state = .error(error) setState(.error(error))
self.notifyError(error) self.notifyError(error)
} }
func fail(_ error: Error) { func fail(_ error: Error) {
// todo specify: failure // todo specify: failure
LoggerProxy.error("\(error)") LoggerProxy.error("\(error)")
operationQueue.cancelAllOperations() cancelableTask?.cancel()
self.retryAttempts += 1 self.retryAttempts += 1
self.processingError = error self.processingError = error
switch self.state { switch self.state.getState() {
case .error: case .error:
notifyError(error) notifyError(error)
default: default:
break break
} }
self.state = .error(error) setState(.error(error))
guard self.maxAttemptsReached else { return } guard self.maxAttemptsReached else { return }
// don't set a new timer if there are no more attempts. // don't set a new timer if there are no more attempts.
self.setTimer() self.setTimer()
} }
func retryProcessing(range: CompactBlockRange) { func retryProcessing(range: CompactBlockRange) {
operationQueue.cancelAllOperations() cancelableTask?.cancel()
// update retries // update retries
self.retryAttempts += 1 self.retryAttempts += 1
self.processingError = nil self.processingError = nil
@ -935,7 +737,7 @@ public class CompactBlockProcessor {
// process next batch // process next batch
// processNewBlocks(range: Self.nextBatchBlockRange(latestHeight: latestBlockHeight, latestDownloadedHeight: try downloader.lastDownloadedBlockHeight(), walletBirthday: config.walletBirthday)) // processNewBlocks(range: Self.nextBatchBlockRange(latestHeight: latestBlockHeight, latestDownloadedHeight: try downloader.lastDownloadedBlockHeight(), walletBirthday: config.walletBirthday))
nextBatchTask() nextBatch()
} catch { } catch {
self.fail(error) self.fail(error)
} }
@ -972,48 +774,9 @@ public class CompactBlockProcessor {
} }
} }
@available(*, deprecated, message: "This static method will be removed soon, use `nextBatchTask()` instead.")
private func nextBatch() { private func nextBatch() {
self.state = .downloading setState(.downloading)
NextStateHelper.nextState( Task { @MainActor [self] in
service: self.service,
downloader: self.downloader,
config: self.config,
rustBackend: self.rustBackend,
queue: nil
) { result in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
switch result {
case .success(let nextState):
switch nextState {
case .finishProcessing(let height):
self.latestBlockHeight = height
self.processingFinished(height: height)
case .processNewBlocks(let range):
self.latestBlockHeight = range.upperBound
self.lowerBoundHeight = range.lowerBound
self.processNewBlocks(range: range)
case let .wait(latestHeight, latestDownloadHeight):
// Lightwalletd might be syncing
self.lowerBoundHeight = latestDownloadHeight
self.latestBlockHeight = latestHeight
LoggerProxy.info(
"Lightwalletd might be syncing: latest downloaded block height is: \(latestDownloadHeight)" +
"while latest blockheight is reported at: \(latestHeight)"
)
self.processingFinished(height: latestDownloadHeight)
}
case .failure(let error):
self.severeFailure(error)
}
}
}
}
private func nextBatchTask() {
self.state = .downloading
Task { [self] in
do { do {
let nextState = try await NextStateHelper.nextStateAsync( let nextState = try await NextStateHelper.nextStateAsync(
service: self.service, service: self.service,
@ -1045,9 +808,9 @@ public class CompactBlockProcessor {
} }
} }
private func validationFailed(at height: BlockHeight) { internal func validationFailed(at height: BlockHeight) {
// cancel all Tasks // cancel all Tasks
operationQueue.cancelAllOperations() cancelableTask?.cancel()
// register latest failure // register latest failure
self.lastChainValidationFailure = height self.lastChainValidationFailure = height
@ -1078,13 +841,13 @@ public class CompactBlockProcessor {
) )
// process next batch // process next batch
self.nextBatchTask() self.nextBatch()
} catch { } catch {
self.fail(error) self.fail(error)
} }
} }
private func processBatchFinished(range: CompactBlockRange) { internal func processBatchFinished(range: CompactBlockRange) {
guard processingError == nil else { guard processingError == nil else {
retryProcessing(range: range) retryProcessing(range: range)
return return
@ -1098,7 +861,7 @@ public class CompactBlockProcessor {
return return
} }
nextBatchTask() nextBatch()
} }
private func processingFinished(height: BlockHeight) { private func processingFinished(height: BlockHeight) {
@ -1110,7 +873,7 @@ public class CompactBlockProcessor {
CompactBlockProcessorNotificationKey.foundBlocks: self.foundBlocks CompactBlockProcessorNotificationKey.foundBlocks: self.foundBlocks
] ]
) )
self.state = .synced setState(.synced)
setTimer() setTimer()
} }
@ -1292,33 +1055,27 @@ extension CompactBlockProcessor {
} }
extension CompactBlockProcessor { extension CompactBlockProcessor {
func refreshUTXOs(tAddress: String, startHeight: BlockHeight, result: @escaping (Result<RefreshedUTXOs, Error>) -> Void) { func refreshUTXOs(tAddress: String, startHeight: BlockHeight) async throws -> RefreshedUTXOs {
let dataDb = self.config.dataDb let dataDb = self.config.dataDb
self.downloader.fetchUnspentTransactionOutputs(tAddress: tAddress, startHeight: startHeight) { [weak self] fetchResult in
switch fetchResult { let stream: AsyncThrowingStream<UnspentTransactionOutputEntity, Error> = downloader.fetchUnspentTransactionOutputs(tAddress: tAddress, startHeight: startHeight)
case .success(let utxos): var utxos: [UnspentTransactionOutputEntity] = []
DispatchQueue.main.async {
self?.operationQueue.addOperation { [self] in do {
guard let self = self else { return } for try await utxo in stream {
do { utxos.append(utxo)
guard try self.rustBackend.clearUtxos(
dbData: dataDb,
address: tAddress,
sinceHeight: startHeight - 1,
networkType: self.config.network.networkType
) >= 0 else {
result(.failure(CompactBlockProcessorError.generalError(message: "attempted to clear utxos but -1 was returned")))
return
}
} catch {
result(.failure(self.mapError(error)))
}
result(.success(self.storeUTXOs(utxos, in: dataDb)))
}
}
case .failure(let error):
result(.failure(self?.mapError(error) ?? error))
} }
guard try rustBackend.clearUtxos(
dbData: dataDb,
address: tAddress,
sinceHeight: startHeight - 1,
networkType: self.config.network.networkType
) >= 0 else {
throw CompactBlockProcessorError.generalError(message: "attempted to clear utxos but -1 was returned")
}
return storeUTXOs(utxos, in: dataDb)
} catch {
throw mapError(error)
} }
} }
@ -1380,6 +1137,7 @@ extension CompactBlockProcessorError: LocalizedError {
case let .wrongConsensusBranchId(expectedLocally, found): case let .wrongConsensusBranchId(expectedLocally, found):
// swiftlint:disable:next line_length // swiftlint:disable:next line_length
return "The remote server you are connecting to is publishing a different branch ID \(found) than the one your App is expecting to be (\(expectedLocally)). This could be caused by your App being out of date or the server you are connecting you being either on a different network or out of date after a network upgrade." return "The remote server you are connecting to is publishing a different branch ID \(found) than the one your App is expecting to be (\(expectedLocally)). This could be caused by your App being out of date or the server you are connecting you being either on a different network or out of date after a network upgrade."
case .unknown: return "Unknown error occured."
} }
} }
@ -1399,12 +1157,6 @@ extension CompactBlockProcessorError: LocalizedError {
} }
} }
extension CompactBlockProcessor: CompactBlockProgressDelegate {
func progressUpdated(_ progress: CompactBlockProgress) {
notifyProgress(progress)
}
}
extension CompactBlockProcessor: EnhancementStreamDelegate { extension CompactBlockProcessor: EnhancementStreamDelegate {
func transactionEnhancementProgressUpdated(_ progress: EnhancementProgress) { func transactionEnhancementProgressUpdated(_ progress: EnhancementProgress) {
NotificationCenter.default.post( NotificationCenter.default.post(
@ -1417,38 +1169,12 @@ extension CompactBlockProcessor: EnhancementStreamDelegate {
extension CompactBlockProcessor { extension CompactBlockProcessor {
enum NextStateHelper { enum NextStateHelper {
// swiftlint:disable:next function_parameter_count
static func nextState(
service: LightWalletService,
downloader: CompactBlockDownloading,
config: Configuration,
rustBackend: ZcashRustBackendWelding.Type,
queue: DispatchQueue?,
result: @escaping (Result<FigureNextBatchOperation.NextState, Error>) -> Void
) {
let dispatchQueue = queue ?? DispatchQueue.global(qos: .userInitiated)
dispatchQueue.async {
do {
let nextResult = try self.nextState(
service: service,
downloader: downloader,
config: config,
rustBackend: rustBackend
)
result(.success(nextResult))
} catch {
result(.failure(error))
}
}
}
static func nextStateAsync( static func nextStateAsync(
service: LightWalletService, service: LightWalletService,
downloader: CompactBlockDownloading, downloader: CompactBlockDownloading,
config: Configuration, config: Configuration,
rustBackend: ZcashRustBackendWelding.Type rustBackend: ZcashRustBackendWelding.Type
) async throws -> FigureNextBatchOperation.NextState { ) async throws -> NextState {
let task = Task(priority: .userInitiated) { let task = Task(priority: .userInitiated) {
// TODO: refactor to async call, issue 463, PR 493 // TODO: refactor to async call, issue 463, PR 493
// https://github.com/zcash/ZcashLightClientKit/issues/463 // https://github.com/zcash/ZcashLightClientKit/issues/463
@ -1467,7 +1193,7 @@ extension CompactBlockProcessor {
downloader: CompactBlockDownloading, downloader: CompactBlockDownloading,
config: Configuration, config: Configuration,
rustBackend: ZcashRustBackendWelding.Type rustBackend: ZcashRustBackendWelding.Type
) throws -> FigureNextBatchOperation.NextState { ) throws -> NextState {
let info = try service.getInfo() let info = try service.getInfo()
try CompactBlockProcessor.validateServerInfo( try CompactBlockProcessor.validateServerInfo(

View File

@ -0,0 +1,183 @@
//
// CompactBlockProcessing.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 10/15/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
extension CompactBlockProcessor {
func compactBlockBatchScanning(range: CompactBlockRange) async throws {
try Task.checkCancellation()
setState(.scanning)
let batchSize = UInt32(config.scanningBatchSize)
do {
if batchSize == 0 {
let scanStartTime = Date()
guard self.rustBackend.scanBlocks(dbCache: config.cacheDb, dbData: config.dataDb, limit: batchSize, networkType: config.network.networkType) else {
let error: Error = rustBackend.lastError() ?? CompactBlockProcessorError.unknown
LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
throw error
}
let scanFinishTime = Date()
NotificationCenter.default.post(
SDKMetrics.progressReportNotification(
progress: BlockProgress(
startHeight: range.lowerBound,
targetHeight: range.upperBound,
progressHeight: range.upperBound
),
start: scanStartTime,
end: scanFinishTime,
task: .scanBlocks
)
)
let seconds = scanFinishTime.timeIntervalSinceReferenceDate - scanStartTime.timeIntervalSinceReferenceDate
LoggerProxy.debug("Scanned \(range.count) blocks in \(seconds) seconds")
} else {
let scanStartHeight = try transactionRepository.lastScannedHeight()
let targetScanHeight = range.upperBound
var scannedNewBlocks = false
var lastScannedHeight = scanStartHeight
repeat {
try Task.checkCancellation()
let previousScannedHeight = lastScannedHeight
let scanStartTime = Date()
guard self.rustBackend.scanBlocks(
dbCache: config.cacheDb,
dbData: config.dataDb,
limit: batchSize,
networkType: config.network.networkType
) else {
let error: Error = rustBackend.lastError() ?? CompactBlockProcessorError.unknown
LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
throw error
}
let scanFinishTime = Date()
lastScannedHeight = try transactionRepository.lastScannedHeight()
scannedNewBlocks = previousScannedHeight != lastScannedHeight
if scannedNewBlocks {
let progress = BlockProgress(startHeight: scanStartHeight, targetHeight: targetScanHeight, progressHeight: lastScannedHeight)
notifyProgress(.scan(progress))
NotificationCenter.default.post(
SDKMetrics.progressReportNotification(
progress: progress,
start: scanStartTime,
end: scanFinishTime,
task: .scanBlocks
)
)
let heightCount = lastScannedHeight - previousScannedHeight
let seconds = scanFinishTime.timeIntervalSinceReferenceDate - scanStartTime.timeIntervalSinceReferenceDate
LoggerProxy.debug("Scanned \(heightCount) blocks in \(seconds) seconds")
}
} while !Task.isCancelled && scannedNewBlocks && lastScannedHeight < targetScanHeight
if Task.isCancelled {
setState(.stopped)
LoggerProxy.debug("Warning: compactBlockBatchScanning cancelled")
}
}
} catch {
LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
throw error
}
}
}
extension CompactBlockProcessor {
func compactBlockScanning(
rustWelding: ZcashRustBackendWelding.Type,
cacheDb: URL,
dataDb: URL,
limit: UInt32 = 0,
networkType: NetworkType
) throws {
try Task.checkCancellation()
guard rustBackend.scanBlocks(dbCache: cacheDb, dbData: dataDb, limit: limit, networkType: networkType) else {
let error: Error = rustBackend.lastError() ?? CompactBlockProcessorError.unknown
LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
throw error
}
}
}
public enum SDKMetrics {
struct BlockMetricReport {
var startHeight: BlockHeight
var targetHeight: BlockHeight
var duration: TimeInterval
var task: TaskReported
}
enum TaskReported: String {
case scanBlocks
}
static let startBlockHeightKey = "SDKMetrics.startBlockHeightKey"
static let targetBlockHeightKey = "SDKMetrics.targetBlockHeightKey"
static let progressHeightKey = "SDKMetrics.progressHeight"
static let startDateKey = "SDKMetrics.startDateKey"
static let endDateKey = "SDKMetrics.endDateKey"
static let taskReportedKey = "SDKMetrics.taskReported"
static let notificationName = Notification.Name("SDKMetrics.Notification")
static func blockReportFromNotification(_ notification: Notification) -> BlockMetricReport? {
guard
notification.name == notificationName,
let info = notification.userInfo,
let startHeight = info[startBlockHeightKey] as? BlockHeight,
let targetHeight = info[targetBlockHeightKey] as? BlockHeight,
let task = info[taskReportedKey] as? TaskReported,
let startDate = info[startDateKey] as? Date,
let endDate = info[endDateKey] as? Date
else {
return nil
}
return BlockMetricReport(
startHeight: startHeight,
targetHeight: targetHeight,
duration: abs(
startDate.timeIntervalSinceReferenceDate - endDate.timeIntervalSinceReferenceDate
),
task: task
)
}
static func progressReportNotification(
progress: BlockProgress,
start: Date,
end: Date,
task: SDKMetrics.TaskReported
) -> Notification {
var notification = Notification(name: notificationName)
notification.userInfo = [
startBlockHeightKey: progress.startHeight,
targetBlockHeightKey: progress.targetHeight,
progressHeightKey: progress.progressHeight,
startDateKey: start,
endDateKey: end,
taskReportedKey: task
]
return notification
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ value: SDKMetrics.BlockMetricReport) {
let literal = "\(value.task) - \(abs(value.startHeight - value.targetHeight)) processed on \(value.duration) seconds"
appendLiteral(literal)
}
}

View File

@ -1,252 +0,0 @@
//
// CompactBlockProcessingOperation.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 10/15/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
class CompactBlockScanningOperation: ZcashOperation {
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
var rustBackend: ZcashRustBackendWelding.Type
private var cacheDb: URL
private var dataDb: URL
private var limit: UInt32
private var network: NetworkType
init(rustWelding: ZcashRustBackendWelding.Type, cacheDb: URL, dataDb: URL, limit: UInt32 = 0, networkType: NetworkType) {
rustBackend = rustWelding
self.cacheDb = cacheDb
self.dataDb = dataDb
self.limit = limit
self.network = networkType
super.init()
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
guard self.rustBackend.scanBlocks(dbCache: self.cacheDb, dbData: self.dataDb, limit: limit, networkType: network) else {
self.error = self.rustBackend.lastError() ?? ZcashOperationError.unknown
LoggerProxy.debug("block scanning failed with error: \(String(describing: self.error))")
self.fail()
return
}
}
}
public enum SDKMetrics {
struct BlockMetricReport {
var startHeight: BlockHeight
var targetHeight: BlockHeight
var duration: TimeInterval
var task: TaskReported
}
enum TaskReported: String {
case scanBlocks
}
static let startBlockHeightKey = "SDKMetrics.startBlockHeightKey"
static let targetBlockHeightKey = "SDKMetrics.targetBlockHeightKey"
static let progressHeightKey = "SDKMetrics.progressHeight"
static let startDateKey = "SDKMetrics.startDateKey"
static let endDateKey = "SDKMetrics.endDateKey"
static let taskReportedKey = "SDKMetrics.taskReported"
static let notificationName = Notification.Name("SDKMetrics.Notification")
static func blockReportFromNotification(_ notification: Notification) -> BlockMetricReport? {
guard
notification.name == notificationName,
let info = notification.userInfo,
let startHeight = info[startBlockHeightKey] as? BlockHeight,
let targetHeight = info[targetBlockHeightKey] as? BlockHeight,
let task = info[taskReportedKey] as? TaskReported,
let startDate = info[startDateKey] as? Date,
let endDate = info[endDateKey] as? Date
else {
return nil
}
return BlockMetricReport(
startHeight: startHeight,
targetHeight: targetHeight,
duration: abs(
startDate.timeIntervalSinceReferenceDate - endDate.timeIntervalSinceReferenceDate
),
task: task
)
}
static func progressReportNotification(
progress: BlockProgress,
start: Date,
end: Date,
task: SDKMetrics.TaskReported
) -> Notification {
var notification = Notification(name: notificationName)
notification.userInfo = [
startBlockHeightKey: progress.startHeight,
targetBlockHeightKey: progress.targetHeight,
progressHeightKey: progress.progressHeight,
startDateKey: start,
endDateKey: end,
taskReportedKey: task
]
return notification
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ value: SDKMetrics.BlockMetricReport) {
let literal = "\(value.task) - \(abs(value.startHeight - value.targetHeight)) processed on \(value.duration) seconds"
appendLiteral(literal)
}
}
class CompactBlockBatchScanningOperation: ZcashOperation {
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
var rustBackend: ZcashRustBackendWelding.Type
private var cacheDb: URL
private var dataDb: URL
private var batchSize: UInt32
private var blockRange: CompactBlockRange
private var transactionRepository: TransactionRepository
private var network: NetworkType
private var cancelableTask: Task<Void, Error>?
private var done = false
private weak var progressDelegate: CompactBlockProgressDelegate?
init(
rustWelding: ZcashRustBackendWelding.Type,
cacheDb: URL,
dataDb: URL,
transactionRepository: TransactionRepository,
range: CompactBlockRange,
batchSize: UInt32,
networkType: NetworkType,
progressDelegate: CompactBlockProgressDelegate? = nil
) {
rustBackend = rustWelding
self.cacheDb = cacheDb
self.dataDb = dataDb
self.transactionRepository = transactionRepository
self.blockRange = range
self.batchSize = batchSize
self.progressDelegate = progressDelegate
self.network = networkType
super.init()
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
cancelableTask = Task {
do {
if batchSize == 0 {
let scanStartTime = Date()
guard self.rustBackend.scanBlocks(dbCache: self.cacheDb, dbData: self.dataDb, limit: batchSize, networkType: network) else {
self.scanFailed(self.rustBackend.lastError() ?? ZcashOperationError.unknown)
return
}
let scanFinishTime = Date()
NotificationCenter.default.post(
SDKMetrics.progressReportNotification(
progress: BlockProgress(
startHeight: self.blockRange.lowerBound,
targetHeight: self.blockRange.upperBound,
progressHeight: self.blockRange.upperBound
),
start: scanStartTime,
end: scanFinishTime,
task: .scanBlocks
)
)
let seconds = scanFinishTime.timeIntervalSinceReferenceDate - scanStartTime.timeIntervalSinceReferenceDate
LoggerProxy.debug("Scanned \(blockRange.count) blocks in \(seconds) seconds")
} else {
let scanStartHeight = try transactionRepository.lastScannedHeight()
let targetScanHeight = blockRange.upperBound
var scannedNewBlocks = false
var lastScannedHeight = scanStartHeight
repeat {
guard !shouldCancel() else {
cancel()
return
}
let previousScannedHeight = lastScannedHeight
let scanStartTime = Date()
guard self.rustBackend.scanBlocks(
dbCache: self.cacheDb,
dbData: self.dataDb,
limit: batchSize,
networkType: network
) else {
self.scanFailed(self.rustBackend.lastError() ?? ZcashOperationError.unknown)
return
}
let scanFinishTime = Date()
lastScannedHeight = try transactionRepository.lastScannedHeight()
scannedNewBlocks = previousScannedHeight != lastScannedHeight
if scannedNewBlocks {
let progress = BlockProgress(startHeight: scanStartHeight, targetHeight: targetScanHeight, progressHeight: lastScannedHeight)
progressDelegate?.progressUpdated(.scan(progress))
NotificationCenter.default.post(
SDKMetrics.progressReportNotification(
progress: progress,
start: scanStartTime,
end: scanFinishTime,
task: .scanBlocks
)
)
let heightCount = lastScannedHeight - previousScannedHeight
let seconds = scanFinishTime.timeIntervalSinceReferenceDate - scanStartTime.timeIntervalSinceReferenceDate
LoggerProxy.debug("Scanned \(heightCount) blocks in \(seconds) seconds")
}
} while !self.isCancelled && scannedNewBlocks && lastScannedHeight < targetScanHeight
self.done = true
}
} catch {
scanFailed(error)
}
}
while !done && !isCancelled {
sleep(1)
}
}
func scanFailed(_ error: Error) {
self.cancelableTask?.cancel()
LoggerProxy.debug("block scanning failed with error: \(String(describing: self.error))")
super.fail(error: error)
}
override func cancel() {
self.cancelableTask?.cancel()
super.cancel()
}
}

View File

@ -8,78 +8,58 @@
import Foundation import Foundation
enum CompactBlockValidationError: Error { extension CompactBlockProcessor {
case validationFailed(height: BlockHeight) enum CompactBlockValidationError: Error {
case failedWithError(_ error: Error?) case validationFailed(height: BlockHeight)
} case failedWithError(_ error: Error?)
class CompactBlockValidationOperation: ZcashOperation {
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
var rustBackend: ZcashRustBackendWelding.Type
private var cacheDb: URL
private var dataDb: URL
private var network: NetworkType
private var cancelableTask: Task<Void, Error>?
private var done = false
init(
rustWelding: ZcashRustBackendWelding.Type,
cacheDb: URL,
dataDb: URL,
networkType: NetworkType
) {
rustBackend = rustWelding
self.cacheDb = cacheDb
self.dataDb = dataDb
self.network = networkType
super.init()
} }
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?() func compactBlockValidation() async throws {
try Task.checkCancellation()
setState(.validating)
cancelableTask = Task { let result = rustBackend.validateCombinedChain(dbCache: config.cacheDb, dbData: config.dataDb, networkType: config.network.networkType)
let result = self.rustBackend.validateCombinedChain(dbCache: cacheDb, dbData: dataDb, networkType: self.network)
do {
switch result { switch result {
case 0: case 0:
let error = CompactBlockValidationError.failedWithError(rustBackend.lastError()) let error = CompactBlockValidationError.failedWithError(rustBackend.lastError())
self.error = error LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
LoggerProxy.debug("block scanning failed with error: \(String(describing: self.error))") throw error
self.fail(error: error)
case ZcashRustBackendWeldingConstants.validChain: case ZcashRustBackendWeldingConstants.validChain:
self.done = true if Task.isCancelled {
setState(.stopped)
LoggerProxy.debug("Warning: compactBlockValidation cancelled")
}
LoggerProxy.debug("validateChainFinished")
break break
default: default:
let error = CompactBlockValidationError.validationFailed(height: BlockHeight(result)) let error = CompactBlockValidationError.validationFailed(height: BlockHeight(result))
self.error = error LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
LoggerProxy.debug("block scanning failed with error: \(String(describing: self.error))") throw error
self.fail(error: error) }
} catch {
guard let validationError = error as? CompactBlockValidationError else {
LoggerProxy.error("Warning: compactBlockValidation returning generic error: \(error)")
return
}
switch validationError {
case .validationFailed(let height):
LoggerProxy.debug("chain validation at height: \(height)")
validationFailed(at: height)
case .failedWithError(let err):
guard let validationFailure = err else {
LoggerProxy.error("validation failed without a specific error")
self.fail(CompactBlockProcessorError.generalError(message: "validation failed without a specific error"))
return
}
throw validationFailure
} }
} }
while !done && !isCancelled {
sleep(1)
}
}
override func fail(error: Error? = nil) {
self.cancelableTask?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelableTask?.cancel()
super.cancel()
} }
} }

View File

@ -0,0 +1,81 @@
//
// FetchUnspentTxOutputs.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 6/2/21.
//
import Foundation
extension CompactBlockProcessor {
enum FetchUTXOError: Error {
case clearingFailed(_ error: Error?)
case fetchFailed(error: Error)
}
func fetchUnspentTxOutputs(range: CompactBlockRange) async throws {
try Task.checkCancellation()
setState(.fetching)
do {
let tAddresses = try accountRepository.getAll().map({ $0.transparentAddress })
do {
for tAddress in tAddresses {
guard try rustBackend.clearUtxos(
dbData: config.dataDb,
address: tAddress,
sinceHeight: config.walletBirthday - 1,
networkType: config.network.networkType
) >= 0 else {
throw rustBackend.lastError() ?? RustWeldingError.genericError(message: "attempted to clear utxos but -1 was returned")
}
}
} catch {
throw FetchUTXOError.clearingFailed(error)
}
var utxos: [UnspentTransactionOutputEntity] = []
let stream: AsyncThrowingStream<UnspentTransactionOutputEntity, Error> = downloader.fetchUnspentTransactionOutputs(tAddresses: tAddresses, startHeight: config.walletBirthday)
for try await transaction in stream {
utxos.append(transaction)
}
var refreshed: [UnspentTransactionOutputEntity] = []
var skipped: [UnspentTransactionOutputEntity] = []
for utxo in utxos {
do {
try rustBackend.putUnspentTransparentOutput(
dbData: config.dataDb,
txid: utxo.txid.bytes,
index: utxo.index,
script: utxo.script.bytes,
value: Int64(utxo.valueZat),
height: utxo.height,
networkType: config.network.networkType
) ? refreshed.append(utxo) : skipped.append(utxo)
} catch {
LoggerProxy.error("failed to put utxo - error: \(error)")
skipped.append(utxo)
}
}
let result = (inserted: refreshed, skipped: skipped)
NotificationCenter.default.post(
name: .blockProcessorStoredUTXOs,
object: self,
userInfo: [CompactBlockProcessorNotificationKey.refreshedUTXOs: result]
)
if Task.isCancelled {
LoggerProxy.debug("Warning: fetchUnspentTxOutputs on range \(range) cancelled")
} else {
processBatchFinished(range: range)
}
} catch {
throw error
}
}
}

View File

@ -1,125 +0,0 @@
//
// FetchUnspentTxOutputsOperation.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 6/2/21.
//
import Foundation
class FetchUnspentTxOutputsOperation: ZcashOperation {
enum FetchUTXOError: Error {
case clearingFailed(_ error: Error?)
case fetchFailed(error: Error)
}
override var isConcurrent: Bool { false }
override var isAsynchronous: Bool { false }
var fetchedUTXOsHandler: ((RefreshedUTXOs) -> Void)?
private var accountRepository: AccountRepository
private var downloader: CompactBlockDownloading
private var rustbackend: ZcashRustBackendWelding.Type
private var startHeight: BlockHeight
private var network: NetworkType
private var dataDb: URL
private var cancelableTask: Task<Void, Error>?
private var done = false
init(
accountRepository: AccountRepository,
downloader: CompactBlockDownloading,
rustbackend: ZcashRustBackendWelding.Type,
dataDb: URL,
startHeight: BlockHeight,
networkType: NetworkType
) {
self.dataDb = dataDb
self.accountRepository = accountRepository
self.downloader = downloader
self.rustbackend = rustbackend
self.startHeight = startHeight
self.network = networkType
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
cancelableTask = Task {
do {
let tAddresses = try accountRepository.getAll().map({ $0.transparentAddress })
do {
for tAddress in tAddresses {
guard try self.rustbackend.clearUtxos(
dbData: dataDb,
address: tAddress,
sinceHeight: startHeight - 1,
networkType: network
) >= 0 else {
throw rustbackend.lastError() ?? RustWeldingError.genericError(message: "attempted to clear utxos but -1 was returned")
}
}
} catch {
throw FetchUTXOError.clearingFailed(error)
}
var utxos: [UnspentTransactionOutputEntity] = []
let stream: AsyncThrowingStream<UnspentTransactionOutputEntity, Error> = downloader.fetchUnspentTransactionOutputs(tAddresses: tAddresses, startHeight: startHeight)
for try await transaction in stream {
utxos.append(transaction)
}
let result = storeUTXOs(utxos, in: dataDb)
self.fetchedUTXOsHandler?(result)
self.done = true
} catch {
self.fail(error: error)
}
}
while !done && !isCancelled {
sleep(1)
}
}
private func storeUTXOs(_ utxos: [UnspentTransactionOutputEntity], in dataDb: URL) -> RefreshedUTXOs {
var refreshed: [UnspentTransactionOutputEntity] = []
var skipped: [UnspentTransactionOutputEntity] = []
for utxo in utxos {
do {
try self.rustbackend.putUnspentTransparentOutput(
dbData: dataDb,
txid: utxo.txid.bytes,
index: utxo.index,
script: utxo.script.bytes,
value: Int64(utxo.valueZat),
height: utxo.height,
networkType: network
) ? refreshed.append(utxo) : skipped.append(utxo)
} catch {
LoggerProxy.error("failed to put utxo - error: \(error)")
skipped.append(utxo)
}
}
return (inserted: refreshed, skipped: skipped)
}
override func fail(error: Error? = nil) {
self.cancelableTask?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelableTask?.cancel()
super.cancel()
}
}

View File

@ -0,0 +1,34 @@
//
// FigureNextBatch.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 6/17/21.
//
import Foundation
extension CompactBlockProcessor {
enum NextState {
case finishProcessing(height: BlockHeight)
case processNewBlocks(range: CompactBlockRange)
case wait(latestHeight: BlockHeight, latestDownloadHeight: BlockHeight)
}
@discardableResult
func figureNextBatch(
downloader: CompactBlockDownloading
) async throws -> NextState {
try Task.checkCancellation()
do {
return try await CompactBlockProcessor.NextStateHelper.nextStateAsync(
service: service,
downloader: downloader,
config: config,
rustBackend: rustBackend
)
} catch {
throw error
}
}
}

View File

@ -1,58 +0,0 @@
//
// FigureNextBatchOperation.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 6/17/21.
//
import Foundation
class FigureNextBatchOperation: ZcashOperation {
enum NextState {
case finishProcessing(height: BlockHeight)
case processNewBlocks(range: CompactBlockRange)
case wait(latestHeight: BlockHeight, latestDownloadHeight: BlockHeight)
}
private var service: LightWalletService
private var downloader: CompactBlockDownloading
private var config: CompactBlockProcessor.Configuration
private var rustBackend: ZcashRustBackendWelding.Type
private(set) var result: NextState?
required init(
downloader: CompactBlockDownloading,
service: LightWalletService,
config: CompactBlockProcessor.Configuration,
rustBackend: ZcashRustBackendWelding.Type
) {
self.service = service
self.config = config
self.downloader = downloader
self.rustBackend = rustBackend
super.init()
self.name = "Next Batch Operation"
}
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
do {
result = try CompactBlockProcessor.NextStateHelper.nextState(
service: self.service,
downloader: self.downloader,
config: self.config,
rustBackend: self.rustBackend
)
} catch {
self.fail(error: error)
}
}
}

View File

@ -1,68 +0,0 @@
//
// ZcashOperation.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 10/27/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
typealias ZcashOperationCompletionBlock = (_ finished: Bool, _ cancelled: Bool) -> Void
typealias ZcashOperationStartedBlock = () -> Void
typealias ZcashOperationErrorBlock = (_ error: Error) -> Void
enum ZcashOperationError: Error {
case unknown
}
class ZcashOperation: Operation {
var error: Error?
var startedHandler: ZcashOperationStartedBlock?
var errorHandler: ZcashOperationErrorBlock?
var completionHandler: ZcashOperationCompletionBlock?
var handlerDispatchQueue = DispatchQueue.main
override init() {
super.init()
completionBlock = { [weak self] in
guard let self = self, let handler = self.completionHandler else { return }
handler(self.isFinished, self.isCancelled)
}
}
convenience init(completionDispatchQueue: DispatchQueue = DispatchQueue.main) {
self.init()
self.handlerDispatchQueue = completionDispatchQueue
}
func shouldCancel() -> Bool {
self.error != nil || isCancelled || dependencyCancelled()
}
func dependencyCancelled() -> Bool {
self.dependencies.first { $0.isCancelled } != nil
}
func fail(error: Error? = nil) {
defer {
self.cancel()
}
if let error = error {
self.error = error
}
LoggerProxy.debug("\(self) failed")
guard let errorHandler = self.errorHandler else {
return
}
self.handlerDispatchQueue.async { [weak self] in
let error = error ?? (self?.error ?? ZcashOperationError.unknown)
errorHandler(error)
}
}
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1787500",
"hash": "00000000013abc1b6771cf8ce5d7b385bef21fdcff3e809e96ad9ea119c0d0fa",
"time": 1661588407,
"saplingTree": "0103e6f1a4409bc20886cfe28eb0d0eb002050edec85b427da3164dd8e8f3d7d4500190000000153f1e09fad18b246d3acf132c9abb67fbb13b5116eeaa226b8553d3e19b25a530000015af73bf615b470dbb8f7d024c13ece622e091cce7bee801e9d54c1ad4e40e02f0149816bfc59f0deaccd1ada4c1e86b19e4413d0721784c4d052c3dc67f5c1805000000001b50664d826ff3b88aaa2ba08c69b8f21dfa710d79860c1441b5c6dfaddae483d00016378d71fb6680e282583c075d721a06b4b3b75d31df554364d4b0251def8a71001936597568d67c917899fd32efdff7afa023fbca108ebcbb402bf0ea9baacf33701f1dfff89a6bedf83c3219c1fd7261883687af993e64603b2b2be8a5fbf89754b01d31d49250f8d4970c03c939f38c3e321a8a96cdaf82a3bee23e73bd7eba7f55a01adf75581cb0b5b7aa87ebed8f1e54ac62217bf676b979298dd8b4300506dd72a01b9e02a948bb911d47d1e51ab6c7050074518cb1f6ff3856f93356d6dde2f8e0700000001f07e315c5670804489d63e01a937077019b5902af6e00ddc3efaea01ef792914000139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "01e6a2858cd4376f3333f54f2033cf1211a3bfc0eb24b9b80ac2cf1446dae2f31c001f0140e2ea00e36545674bc7f09751f3391d628c78399ba803b6173e9d738a5fcc320000000179b60c3bf1b8cce227df010f5c49c22c7d777d5209738683c9b697756b31a81f016f91c16f2e3960269dac6fb301b534e027bb03374126e5dbf5e202c7422c861800011e71b797efd14accdf8a33abde065d670e6fa02a777d9e7d47be234e86b5431701deb97e143f39b0820e3421fd8c38cd340bc9cfac21262ddf86e0eafd75ff570a01b5045572a2467061c9282dea3618ba48f5b6dc05d83d1ad38d57e70735e8b514013b5384e5504f03e481ba341a2f14cd449a0a390acfeb5c56a155f6d99e188813000001794870c4958f10ee3be337bd2bd6ff6784d19662c242a40ad54a3f48bb2523230001bdd331201a0524ecdd6f812cf323d1fe0173f165c181bd74199480a6211bb91f01782a84e2fe01efa2b41d6875b0537d55ead77c75811ee929f0cf9236f976383700019886882316b2f5a810dda1ece4d983ed360175e2cc5ed2b3781ee1d9f1e05f2801081565fb9592a4fab10edbfb15d2aa45c89eb85837f4f298c586cb21d45b772901f791f26a6ace2c09ba3deee8db26c3299e8e9ea6f4fdcac0846b2121721c310100000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1790000",
"hash": "000000000100dac22c6e557360ef3c4283d9f60c6f4c5a1e0076d3b1fe97d0d4",
"time": 1661777577,
"saplingTree": "019d3cd586fd43ea7b42b862cf823edcd874462ed3a955f1c1dfd371d86dccd5700019000000015df1e3c5a797bbbe4f4848f60f21c0bb5f7bbf3a3168cee8a1cf5689b22286590001144654b0777c00cbe78b716bacc819d39f13945657bea3ff7e0c88bf1f68894d0001164cf0832981b185067cef308aefc2cc7c93a218b7ef456b3813f58ec2162d4301ec4614750b2c957a25a24685bbd536845cebe0739d3baad2ce645105e8584d5d000168ac0ff0fb620aab894183850de738f2a79424b36e11d5daf6a77e68b771081e00012a04464db06df9410ca6b17e29f72c4b4b3c5795cee583c5422168eabb7e84210001e8e58e5db11fbfce460285fef5113a83e8b5f0ac542ce3887d722a8e2a413c2f0001fd8156cb2a632eb0265b6f49309d30a762bdbbe2e8d6148ed26ab69e6861472a01187bf80dcc94180caeacb5860ca78ef66b0977c7ef6b773428a3e19b676aeb47014e7c8a253ae2f1d791c4bb7f5026736ea71839162c1b1da41cb6a6e64a49261e0001367383639b55ca56d7008a7a64d73b875703860d1a657cc1cc0df5a9ae5d5c2b0001f07e315c5670804489d63e01a937077019b5902af6e00ddc3efaea01ef792914000139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "017fed4159767f6bfb26e8136b85e4a179606f7298de17664fdcc220df21b7a02d001f01cdd388ecf7f914294552caad1d570970b47974ec73494e2f04a80e7b391afc10011f6a01e993dbacd0a5487b8a7b06275a252d0d3895fab490e2030a255a9b0c06000000012c0afb3c10723bacf85c6611273be9e7d4676128dbabec978f0fa5221223893600000001285b5cb86d8544386b6a36c49e2bdb88fbd5f59a43437cfe271d95dfb4bcf82e01b20850820c9e07dfee4fd137ebe7f4d92ff7e318f3f1581b4b53014118d6d1170000000001045adfb00ce9f9ceddcebe2658b24be1beb059888c4f93f68bde2615aca3bf0c010d67deaf001eabcb4ded2f92fd69bb00fea4118f38c2b18b9dc2ae7e8c52480701f6a82e767e738485f26942fae63d20665479e996d8404cf44a451f9585fc5b32019886882316b2f5a810dda1ece4d983ed360175e2cc5ed2b3781ee1d9f1e05f2801081565fb9592a4fab10edbfb15d2aa45c89eb85837f4f298c586cb21d45b772901f791f26a6ace2c09ba3deee8db26c3299e8e9ea6f4fdcac0846b2121721c310100000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1792500",
"hash": "0000000000feda98d580a542579c369cbedb9677e2a60b45858dd3c3970aec48",
"time": 1661965969,
"saplingTree": "01056e79c206e1ec677117980ba8ffd3109f8df1911bf60d6a1f17194ca42d7a23018271f1b6a7b1034a1cb52e214e01fa76fe9b57b4ecc51513fdfee851a552e55c1901d12b2f51a9766ca6bb0e39e2fb8f15d3d27a620b7807b156df4d5519b115c239000166bee33838512fb2a13169dbec4a023d71eddccda1f6f364429b9012970e430f0001f498d5859edb460c87552d732c800e3cb655f87dd39edfb1123ee44f170e6b710001b8ea5420dbf802c4aa031ea066906c68e0b134ecd9f7ce1a8a9595301621b1270159b5fea41595326ec50f60bb37bc915c5525204dbb077ee6096a733e5e6be95d0001a18b7820f47bf1fe5304af022fa5c0dbe4d6d2b3c2b8f43d82e89bd29603ee3e0001ef928eac8166f4fa704d28411595a6d9e48fbce2fdb0cc1c22ff5eb65b8ea60301f62c314465ee352a914c74a495044a37f32ed19057b1e8b1139c24ae5fe1eb6d000001a54b39b5536ee32eaccd615c97b5894e59ca339fa58b170542e565957973c2640001abc74ef752650de527e13ef419675a18fa8abc0ea648bb79c9bad4ddc3a3c33300000001707d2f9501c9e36a368499bad696bc5aaa721b6d08f9a40f4ec8851e9deb373401f07e315c5670804489d63e01a937077019b5902af6e00ddc3efaea01ef792914000139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "01d12a9e0d3c96789f73acf81fbbd59e1352c9510eddd29d7b3e79fdb6c34e873801ac79591488cf371014ca4577af93bf3b81be2312c23315f0d30007fee8d5a22e1f0115bacc61ac12c6c35f4601d1fa17909700158b676b305700615c8b22c50c0f11012075bbb910e3877bea5dfd4594729900eef32aa66effa3aaa2354a455a507f1801f5a06cb7bb2283e8e1bb8e5eb22ac57741f666f1c5ef2db8e5f644957c86481e0001f4924dc1afdcef8b935fda84922dab90e523b0100e329d24295e4ab809f38d19012ce7342f56d5319d6c54a63a326e33b8dced10d6cd696abf270e8305d8b6101c01e05e25ecc131faff939ad965861a12eda859ef69fd6928c65c978ae0d827592301d646e453f3ee10362226de45b8e9377ac12191351816efdf5d3254668345422e01b2642250d10f8ec40181ee561a18b60870aa700977107805b91d2f52b872f122012eb7bc0fff59658e28f1bf9b79fa184d21ff2679637745b2e6c51657993e001b01a50a617a7ffe94d3b52041db8dd9a0830c2fe42e394ce95ad4b6265bae2c7112000001f901961738a6f66400aee2908298ed5b83c53adea294db77b57d10eedbfcb5370187ae41d92d3780530a81c2db1a35768479f5843a62c64846320039681551062e00000000000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1795000",
"hash": "0000000000080087f9812e87732355691e4bdd43fdbbf517618959c81cccdda1",
"time": 1662152700,
"saplingTree": "01b68aa765ae73bf3eddef03a2a0127126964d9a543859d462540467a4772425610019000001aa8687526bc08a818db298cf940aa78c30792a927a58ee1b6d11715b2507d9280000013788d7a49d2a299ce893eeb4175e702b4380f070ccd32d4b4beebdaae9d2dc3d00012653cc481a3bd5901dd5728d9754f9555932c00647c15ebd5a13cb1db579383301764ef57a84dee173a7d8b135065b75be0b012d5931af754bd339f6572ad84a5a01642fd0ec665cb43eff91032c290ee9cccf50b234af0f5ea94798973b3efc392f000190a2aa5d8963fe49141ced07138011754b00493211523505d488a2cf3ba24a320168569b74e5b9370152695d797cbe7174538b8e33304539f72efef6b58a9a4f04010b05111ee7d748441446ecc08f652247197356bbf83d73d7d07448ed4754192b017e137de7b7b21bbf9c3f9720196427864e7fe2fd8070aba9237b24fbec95914a0113c1af933a4048a7b09cb0e6ccd27691f4649385596d8111fe602bf4b255db29010470b81e369a600bdd7d6bb96c409554913044eb1c66dd7659a6e68129136165014d7bd573014ae0f6d462262ec28df786ef65aa9bcd5e2d6c54f26b4ee3633c2b0127bd15f9da068d7c81f1396a0ee87b700755451b9f4cebfa11919f3a4285713c016bce4a3d7e7ae87037b3ba0b9789dd1a1737e355cb862aba330cec03aec244710001707d2f9501c9e36a368499bad696bc5aaa721b6d08f9a40f4ec8851e9deb373401f07e315c5670804489d63e01a937077019b5902af6e00ddc3efaea01ef792914000139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "012d8880e630c07430af4529f928e8d5d6d7f19f69a4449e7b6ce739f73a2e2b2f014fc16e1b120f140566bfd6a4e40797c72c87af205508ae98259bcdba377d3a0d1f01caa4adc8466eb027a6c6095ab1a20fecbe058ef63db381fd1d1f08628bb5a41801235d173241458aebb1b6d757489969e24a1a09720606ba56b1eae566bfbdb01300000126b2135c0942f13da6de1470ee2e5bc26c98f8d6d808ee71f2e7d7683ada763400000171071f52e8986c28e68db191de9cf8b88e39af84909289207ca4f56d346e85160000018f9f481c4fb6151f6868afef177dbd28f208ddb31bc4951185f31f4b6996420d014e57bc25f0a5322059b756e0b4bea83442a5e418d3486b57c617757d9098552501e0e364cb2abe4d1967bca145c4af1774eb2d84bca3af756581870661685cfb160001eb3af18b241fa811548c29205f42601bee7b58ba55fc529542836f1cf477bd080001f63a616ca09e1106337cee385439f0bdb7e14ebf6d2b3eaafc4c69ed04f147300000000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1797500",
"hash": "000000000062762b9e9de093b9375fe1bfc451ff05a2c11a0339c90ec3e879b0",
"time": 1662341718,
"saplingTree": "0125d05417c5144dfe8aec67961d71a700c93b51d82486803f8c4719be2708274b01c675445684630272ec881b8283397137140ef8089bac18b3f07b0a7f3f8f6c54190192d55e6fef209eede1572a74f52fb42e347bb17221b179f28443e8ac190e480701489b0714407a588eaa9b6be421f77049a98bb005b79c3df1f8d592201360f94a000000000163669ec32a05e1726bfd63268924daf5bb3f6794a05a7824f87492708cbc3b2a0001e4bfc91695ef7bce9e29f48595b8cc278af7b1857039f2fdcf75bd58b579bc6201b6a313b5daf1ebb237d55661ce5d6f6a93018b407f2386635b727bdfe8ea6019000001dfe1241acfb16d56a4163ad86a0bb7b763b4f5da28b17b4413a3e6e8dbb7d728019a4fe2a045f95d86fe1a5fa9d413c4f36e42e1f03fbb2b5bed842d96d3f71e1b018e123a755b1d549c6f87ac048fa15664bbc3b10a0ef155b046eb4bc7e5b7061d000000000000000001c5d9822e7aa76d3a758e8dc25ffd8b8c9d4051c66fb92fd7aa263905e238920e0139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "01a23430438ea5dafb4b7b4db2111f6280226cf69c172a8a763d1218f92ed6c23a001f01e4aeacad6fe90d4122dffbad65537ce856480aadcca1866d9682b2b968cc5e2f000001d142373141d5e6a48be5d097631fec923007c886d34e2b1ff5fbb55fa4f33810010f5906e6e3c4bf7d8015ff88c532795ea4b49f35dbb41a6bdf8592d09439a92c01c6580c00bd5c6da40f2026554c86788192aa5e8cd5f7486d8d51cc258cb312200001e41507a73031aa566ffb009506cd5b30992b0ebd34fc3fc78449a886a4317a0d0000015b224fa4142c9508f0b2dfd1e64fff63e996b697a4dd9502f9a3fd917cbfc42200010fbd0f1d4f428961e0b32b28994bfdd80e1e68b330883bd0889f9ca8f948b6190136b0d3765a45a227aedb1925c5670bc6364856ffcbaf19b52ab06f179ed897350132fe04f3088f035c4ca4c818b3e720ac26ac172f30b27dcd3e3bbcbb8192c11001fe9e4b5837bc7acc9356055fda92475bd0593e36a180e93549bb1ce2ab52e20e01f63a616ca09e1106337cee385439f0bdb7e14ebf6d2b3eaafc4c69ed04f147300000000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1800000",
"hash": "0000000000f908c93f3a7f4288b1f73a5c5621397ae94e4a894ef44ecceaa961",
"time": 1662529994,
"saplingTree": "01394fa4810bafa4d2231cb3dcc560e43e9f02418e82635d6a3619276e76407118014a496ba29efc52eaf66430ebe6be9d9b8dc10ab790cf624bd9b16d404e32e52619017ce5fbd2d63aee2d62820d40951f1efdb1ea9d4b7b3c40470d44b61b3537303c01b4c88e366d13efe090502c5e1c147a70f9be10226d97a16f726aef466245a9400001ecaf654b7bb55264015e8e5cf8ca20a01f79dafea9ea174a216727160a3596180128dfcd78a6488832dedf7d4bc7af36360a3ced5ac10f26a49451d2543096672b0001867d8b2f5148a10776a109b8cc6c0fbe006256eddd007ecdc8ac703ae03db126000001836b32ac862719c718b58435f87bba6d66a476711854121212299657ab973d440120d4f1b55c4e9e43442c08a201d4518369927b976a61c12add79cfdcd62c515000000109545546d941eb229dba133374ec57484d65febeffed0eaf9101e039db7eb06d01f98dd99c797f8ff594d93854a1f42f17bcf85d22e75fa9a06ce53b395ae1e671000148f810d1aaaa173f8c744fca6efe293f1b7be578def47bd9e45e647a35df5e3001d224af2c322521ed4297213b51e327cfa001cb6b7c04fd647a8c60d5ecff6717010bfec213bc4e3bfb6039ad8e0d01f1b6662367b4e38a2b185f227d77591b3b2001cf2ef2a7179af6eb4d2552757c05466c345a169d15c5da6dc13ba8fd34e3495700000001c5d9822e7aa76d3a758e8dc25ffd8b8c9d4051c66fb92fd7aa263905e238920e0139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "01e0e4bfc911716e9548d6d4cb84cf8b66f62df3647a176f9aa3c66453013ff206010b0e83345c27139830a0fff89f018987eaf3b6ed776ea138ab846e064c7685041f0001821067594b408dbac72a9967d0d135053f174f0adc604af6887867ba1a1f0735017f46cc72632f201b8854c67456f91fe83418040e8be1bf983231da9d4e8c1a0000013a6f5f208f42b3640fe9a182cc4653f0b46cf1c62f7ca31ffcd14a542d8f7917016560e4ea4138ea5bcae07924ce84454871d1da8efb6ce4a59acb78f3287a30140001cb756a9be049d541b1cb9d4e58344c3cc6a34863345ac0c122744c026f29f7200000000000000001c5633b986425941f3be7634113839a2de1d40a43529148a16fc979cfd2245d310001c31609e44f34c7d006f03facc5589989fc465061ad30e83b7d4f61a3dc2d353200000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1802500",
"hash": "00000000017fbac093c9dd6d349b88685f369cbbe2c12b9a0c9ed6830bfc8e91",
"time": 1662718546,
"saplingTree": "018672213d179722fdc5221d5199441fc70a068db8c20a02ebf8bec70dac9f4e08017a0d68f3d30e1f5e3690c627ead792ae7ce1d08761b095ed717db285e60a0166190001c44e548845f6a825e9fdcdcec964674a84c48cff34c112d9828c1d5f21a5502a00000000014aea74e6cc2dbae2ff085618b475820a68a5969215246e00353f68129994a72900011489a9121bb1593a210f489a44b3fb8e2fe7b0b1143d89cf338b1da874438149012ea4d7a0b0e68673080d9f47f4c92728839fe0991626ae337f9ad6bfa598872b000001629e5d548ad857baaa6716719e67708b8b696b77b23ef900d49820d028ac604101cf6d58fea133cccc9307a56ef90ca4f8b32af3fb8b494b583724275da946a1370000000001cddcf07098de80d473262d9e1594738168a73a2e27bb986b9ce8a43635ccd03d00013f6c68973c08f026717c32a6be0d4083febb8c2d63f2e895a6d07021afa3a51c000001c5d9822e7aa76d3a758e8dc25ffd8b8c9d4051c66fb92fd7aa263905e238920e0139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "011618cffe419b3ae6d6f94ed2138925766f616262a81b11220dd64310db9a602201b50f80ddf6be2c181b9e4795a48aa30def25318410e19071bbcae266517237061f01000ec86443fda797bdee68d22ad1c8e3159ce016343aed4bdc4044b30808ff0d000192dd02c90b3e87a42dc8a5a1bc59b832e21937614d71aec0ca0dcb4293c8802e000000015226ac6b1c91f0b90680b4cf4175a38450727aeaec887792adb9ef13c411de0b0001bc359c17ecd3d5fb2896bfcf890ebc51de0eda3c55734dc24523a41b24267f1a0001c8b40d0cea90b7642884cdca72286ca44b278ae6e9ad2e2bbee1b30680b1a60d00010deddae26bf93da080b26610dae9ddccb9243015487b922f3c05198476c4ca2400019902d40b5aab956a3001b26499f3f689c08ffc9b5989d14840ea01f443dfe61c01c5633b986425941f3be7634113839a2de1d40a43529148a16fc979cfd2245d310001c31609e44f34c7d006f03facc5589989fc465061ad30e83b7d4f61a3dc2d353200000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1805000",
"hash": "00000000011cfeaec0ee1d6230aa8d3642ad4c22db6d541ff43520ed2035f7eb",
"time": 1662907506,
"saplingTree": "016d31b4c64ae5c17308b9d90572e6de5993b3755e861e040949fe4438caf1000e00190000016c0be2c033839aec91d32becbaf583282f918ad917865f323a5c88d687f1bb1501ba7e5ccb08c9579b4fa81fa555c3fa94e6872eee2bf8f01641f6103e9a975b4b019c049c4abf367e07e168ba327e86a7cb04d11d9ad446e2356f2870cfe9e958340001f29ca1e8f612cc422bfeb2713a89062d8695f44436cb8ac0b57ff5f2f1d8da5401e4195653dc559c9590e0248719443c666c82d6e7521d2688d32a914070eb7a540001f8d79dde2f627bfd60d0c1131cd3ad6b779c9619f6dbb0ab0515778a9a9fe24d0000000001235b463d441fae8a4ee172bb40b1a3853ba4a07ea44377b06295db356608ea3a013c2d33c9c5afba6b720ffd9ea7c69af1635c2f98d291e2eaf358f2c87232ec2a0000013ec9722bf3aeef59c292db71e3de87d2a3868cdd2902b867328e24c30c8dbe1b0000018c2d6adea2ad4faf20eccfc2c2a2c59192fb53d3204b3a2757f1c247dadec16b0001c5d9822e7aa76d3a758e8dc25ffd8b8c9d4051c66fb92fd7aa263905e238920e0139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e",
"orchardTree": "01a1eac07fe90382f2003de69239f281a9798b7f984bc20a3559d5f886a9e07035001f019f6fb5bad161ee9f26dbe23c485e0f25d083e17c1e68b91525f33d659d35ac1d00013957a17ebe53945197d684304f29a77486a0f081c6a4a3f4c73b33b88a834a1101e151d0e8b30866bf9779bb3d1105c3124db863690458c855537e967679bd903c000000000001bad8cfde5bec689cc645ed5c9e37f6e6f1753fa9a95f40ccfe25ac91bcc2481e019927318c01f928c75112b5d0c116e9a7244d58684396512e5e24569acb94581a0000010ca90449f94a0ea6eb64ff88b438e14649280613827022401d54a8635e63943100011759fa02e2fae57b40cd62bd3829225c15bad6430caf25a526d51f329091293e014660573ed7dbd889063d06f1d814a4f28a5ba4da76c2c5c8e11ad980f070353601c31609e44f34c7d006f03facc5589989fc465061ad30e83b7d4f61a3dc2d353200000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000"
}

View File

@ -369,7 +369,7 @@ extension LightWalletGRPCService: LightWalletServiceNonBlockingAPI {
} }
continuation.finish(throwing: nil) continuation.finish(throwing: nil)
} catch { } catch {
continuation.finish(throwing: error) continuation.finish(throwing: error.mapToServiceError())
} }
} }
} }
@ -551,7 +551,7 @@ extension LightWalletGRPCService: LightWalletServiceNonBlockingAPI {
} }
continuation.finish(throwing: nil) continuation.finish(throwing: nil)
} catch { } catch {
continuation.finish(throwing: error) continuation.finish(throwing: error.mapToServiceError())
} }
} }
} }
@ -619,7 +619,7 @@ extension LightWalletGRPCService: LightWalletServiceNonBlockingAPI {
} }
continuation.finish(throwing: nil) continuation.finish(throwing: nil)
} catch { } catch {
continuation.finish(throwing: error) continuation.finish(throwing: error.mapToServiceError())
} }
} }
} }

View File

@ -175,7 +175,7 @@ public protocol Synchronizer {
func latestHeight() throws -> BlockHeight func latestHeight() throws -> BlockHeight
/// Returns the latests UTXOs for the given address from the specified height on /// Returns the latests UTXOs for the given address from the specified height on
func refreshUTXOs(address: String, from height: BlockHeight, result: @escaping (Result<RefreshedUTXOs, Error>) -> Void) func refreshUTXOs(address: String, from height: BlockHeight) async throws -> RefreshedUTXOs
/// Returns the last stored transparent balance /// Returns the last stored transparent balance
func getTransparentBalance(accountIndex: Int) throws -> WalletBalance func getTransparentBalance(accountIndex: Int) throws -> WalletBalance

View File

@ -194,7 +194,7 @@ public class SDKSynchronizer: Synchronizer {
return return
} }
blockProcessor.stop(cancelTasks: true) blockProcessor.stop()
self.status = .stopped self.status = .stopped
} }
@ -639,8 +639,8 @@ public class SDKSynchronizer: Synchronizer {
} }
} }
public func refreshUTXOs(address: String, from height: BlockHeight, result: @escaping (Result<RefreshedUTXOs, Error>) -> Void) { public func refreshUTXOs(address: String, from height: BlockHeight) async throws -> RefreshedUTXOs {
self.blockProcessor.refreshUTXOs(tAddress: address, startHeight: height, result: result) try await blockProcessor.refreshUTXOs(tAddress: address, startHeight: height)
} }
@available(*, deprecated, message: "This function will be removed soon, use the one returning a `Zatoshi` value instead") @available(*, deprecated, message: "This function will be removed soon, use the one returning a `Zatoshi` value instead")
public func getShieldedBalance(accountIndex: Int = 0) -> Int64 { public func getShieldedBalance(accountIndex: Int = 0) -> Int64 {
@ -856,6 +856,7 @@ public class SDKSynchronizer: Synchronizer {
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError) return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
case .saplingActivationMismatch: case .saplingActivationMismatch:
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError) return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
case .unknown: break
} }
} }

View File

@ -1,270 +0,0 @@
//
// BlockScanOperationTests.swift
// ZcashLightClientKitTests
//
// Created by Francisco Gindre on 10/17/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import XCTest
import SQLite
@testable import TestUtils
@testable import ZcashLightClientKit
// swiftlint:disable implicitly_unwrapped_optional force_try force_unwrapping print_function_usage
class BlockScanOperationTests: XCTestCase {
let rustWelding = ZcashRustBackend.self
var operationQueue = OperationQueue()
var cacheDbURL: URL!
var dataDbURL: URL!
var ufvk = UnifiedFullViewingKey(
validatedEncoding: "uviewtest1q48t999peecrfkq7ykcxckfkjt77w3lckk5mptlrtuy7xltjnzg8fm5434cxe9p9838ljs24yv83rluhk33ew098dkarapzyj4vk5kfxp5zn2jp3ww74jwd48r05aqjvgqxzx3nqn6zfqh3cmwdtmz0mc5624tvdza55q7mguxrehwcy4y0uktcpp4tkpex4qhazddux4yt6hr0sc9fkqmfr5tyz6ldd7yrq93tyj7446u4kst3vhmd40uga636p56hr0hjfdhgp07qyh90kmsl3qnmld6c8h7u06vekkjywmxv07mqzz9muwcl6weczrn5vf3p27uc9ufrumdp64zdzulzvc373wx3gl0yntntujhcsjhrwk9xwyjpvyuf0s8q3mgjs7uy3pg960w40dthpngcnauhgg9xq8cdcyfkq7ctnngqg4nkp5eh9knd4ckwjyd9czdd240lumul96r2fuerlvjeha6cyn9ftm7gr6xqjmq0zy6tv", // swiftlint:disable:this line_length
account: 0
)
var walletBirthDay = Checkpoint.birthday(
with: 1386000,
network: ZcashNetworkBuilder.network(for: .testnet)
)
var network = ZcashNetworkBuilder.network(for: .testnet)
var blockRepository: BlockRepository!
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
self.cacheDbURL = try! __cacheDbURL()
self.dataDbURL = try! __dataDbURL()
deleteDBs()
operationQueue.maxConcurrentOperationCount = 1
}
private func deleteDBs() {
try? FileManager.default.removeItem(at: cacheDbURL)
try? FileManager.default.removeItem(at: dataDbURL)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
operationQueue.cancelAllOperations()
try? FileManager.default.removeItem(at: cacheDbURL)
try? FileManager.default.removeItem(at: dataDbURL)
}
func testSingleDownloadAndScanOperation() {
logger = SampleLogger(logLevel: .debug)
var dbInit: DbInitResult!
XCTAssertNoThrow(try { dbInit = try ZcashRustBackend.initDataDb(dbData: self.dataDbURL, seed: nil, networkType: .testnet) }())
guard case .success = dbInit else {
XCTFail("Failed to initDataDb. Expected `.success` got: \(String(describing: dbInit))")
return
}
let downloadStartedExpect = XCTestExpectation(description: "\(self.description) download started")
let downloadExpect = XCTestExpectation(description: "\(self.description) download")
let scanStartedExpect = XCTestExpectation(description: "\(self.description) scan started")
let scanExpect = XCTestExpectation(description: "\(self.description) scan")
let latestScannedBlockExpect = XCTestExpectation(description: "\(self.description) latestScannedHeight")
let service = LightWalletGRPCService(
endpoint: LightWalletEndpoint(
address: "lightwalletd.testnet.electriccoin.co",
port: 9067
)
)
let blockCount = 100
let range = network.constants.saplingActivationHeight ... network.constants.saplingActivationHeight + blockCount
let downloadOperation = CompactBlockDownloadOperation(
downloader: CompactBlockDownloader.sqlDownloader(
service: service,
at: cacheDbURL
)!,
range: range
)
let scanOperation = CompactBlockScanningOperation(
rustWelding: rustWelding,
cacheDb: cacheDbURL,
dataDb: dataDbURL,
networkType: network.networkType
)
downloadOperation.startedHandler = {
downloadStartedExpect.fulfill()
}
downloadOperation.completionHandler = { finished, cancelled in
downloadExpect.fulfill()
XCTAssertTrue(finished)
XCTAssertFalse(cancelled)
}
downloadOperation.errorHandler = { error in
XCTFail("Download Operation failed with Error: \(error)")
}
scanOperation.startedHandler = {
scanStartedExpect.fulfill()
}
scanOperation.completionHandler = { finished, cancelled in
scanExpect.fulfill()
XCTAssertFalse(cancelled)
XCTAssertTrue(finished)
}
scanOperation.errorHandler = { error in
XCTFail("Scan Operation failed with Error: \(error)")
}
scanOperation.addDependency(downloadOperation)
var latestScannedheight = BlockHeight.empty()
let latestScannedBlockOperation = BlockOperation {
let repository = BlockSQLDAO(dbProvider: SimpleConnectionProvider.init(path: self.dataDbURL.absoluteString, readonly: true))
latestScannedheight = repository.lastScannedBlockHeight()
}
latestScannedBlockOperation.completionBlock = {
latestScannedBlockExpect.fulfill()
XCTAssertEqual(latestScannedheight, range.upperBound)
}
latestScannedBlockOperation.addDependency(scanOperation)
operationQueue.addOperations(
[downloadOperation, scanOperation, latestScannedBlockOperation],
waitUntilFinished: false
)
wait(
for: [downloadStartedExpect, downloadExpect, scanStartedExpect, scanExpect, latestScannedBlockExpect],
timeout: 10,
enforceOrder: true
)
}
@objc func observeBenchmark(_ notification: Notification) {
guard let report = SDKMetrics.blockReportFromNotification(notification) else {
return
}
print("observed benchmark: \(report)")
}
func testScanValidateDownload() throws {
logger = SampleLogger(logLevel: .debug)
NotificationCenter.default.addObserver(
self,
selector: #selector(observeBenchmark(_:)),
name: SDKMetrics.notificationName,
object: nil
)
let dbInit = try self.rustWelding.initDataDb(dbData: dataDbURL, seed: nil, networkType: network.networkType)
guard case .success = dbInit else {
XCTFail("Failed to initDataDb. Expected `.success` got: \(dbInit)")
return
}
guard try self.rustWelding.initAccountsTable(dbData: self.dataDbURL, ufvks: [ufvk], networkType: network.networkType) else {
XCTFail("failed to init account table")
return
}
try self.rustWelding.initBlocksTable(
dbData: dataDbURL,
height: Int32(walletBirthDay.height),
hash: walletBirthDay.hash,
time: walletBirthDay.time,
saplingTree: walletBirthDay.saplingTree,
networkType: network.networkType
)
let service = LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.eccTestnet)
let storage = CompactBlockStorage(url: cacheDbURL, readonly: false)
try storage.createTable()
let downloadExpectation = XCTestExpectation(description: "download expectation")
let validateExpectation = XCTestExpectation(description: "validate expectation")
let scanExpectation = XCTestExpectation(description: "scan expectation")
let downloadOperation = CompactBlockStreamDownloadOperation(
service: service,
storage: storage,
blockBufferSize: 10,
startHeight: walletBirthDay.height,
targetHeight: walletBirthDay.height + 10000,
progressDelegate: self
)
downloadOperation.completionHandler = { finished, cancelled in
XCTAssert(finished)
XCTAssertFalse(cancelled)
downloadExpectation.fulfill()
}
downloadOperation.errorHandler = { error in
if let lwdError = error as? LightWalletServiceError {
switch lwdError {
case .timeOut:
XCTAssert(true)
default:
XCTFail("LWD Service error found, but should have been a timeLimit reached Error - \(lwdError)")
}
} else {
XCTFail("Error should have been a timeLimit reached Error - \(error)")
}
}
let validationOperation = CompactBlockValidationOperation(
rustWelding: rustWelding,
cacheDb: cacheDbURL,
dataDb: dataDbURL,
networkType: network.networkType
)
validationOperation.errorHandler = { error in
self.operationQueue.cancelAllOperations()
XCTFail("failed with error \(error)")
}
validationOperation.completionHandler = { finished, cancelled in
XCTAssert(finished)
XCTAssertFalse(cancelled)
validateExpectation.fulfill()
}
let transactionRepository = TransactionRepositoryBuilder.build(dataDbURL: dataDbURL)
let scanningOperation = CompactBlockBatchScanningOperation(
rustWelding: rustWelding,
cacheDb: cacheDbURL,
dataDb: dataDbURL,
transactionRepository: transactionRepository,
range: CompactBlockRange(
uncheckedBounds: (walletBirthDay.height, walletBirthDay.height + 10000)
),
batchSize: 1000,
networkType: network.networkType,
progressDelegate: self
)
scanningOperation.completionHandler = { finished, cancelled in
XCTAssert(finished)
XCTAssertFalse(cancelled)
scanExpectation.fulfill()
}
operationQueue.addOperations([downloadOperation, validationOperation, scanningOperation], waitUntilFinished: false)
wait(for: [downloadExpectation, validateExpectation, scanExpectation], timeout: 300, enforceOrder: true)
}
}
extension BlockScanOperationTests: CompactBlockProgressDelegate {
func progressUpdated(_ progress: CompactBlockProgress) {
}
}

View File

@ -0,0 +1,191 @@
//
// BlockScanTests.swift
// ZcashLightClientKitTests
//
// Created by Francisco Gindre on 10/17/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import XCTest
import SQLite
@testable import TestUtils
@testable import ZcashLightClientKit
// swiftlint:disable implicitly_unwrapped_optional force_try force_unwrapping print_function_usage
class BlockScanTests: XCTestCase {
let rustWelding = ZcashRustBackend.self
var cacheDbURL: URL!
var dataDbURL: URL!
var saplingExtendedKey = SaplingExtendedFullViewingKey(validatedEncoding: "zxviewtestsapling1qw88ayg8qqqqpqyhg7jnh9mlldejfqwu46pm40ruwstd8znq3v3l4hjf33qcu2a5e36katshcfhcxhzgyfugj2lkhmt40j45cv38rv3frnghzkxcx73k7m7afw9j7ujk7nm4dx5mv02r26umxqgar7v3x390w2h3crqqgjsjly7jy4vtwzrmustm5yudpgcydw7x78awca8wqjvkqj8p8e3ykt7lrgd7xf92fsfqjs5vegfsja4ekzpfh5vtccgvs5747xqm6qflmtqpr8s9u")
var walletBirthDay = Checkpoint.birthday(
with: 1386000,
network: ZcashNetworkBuilder.network(for: .testnet)
)
var network = ZcashNetworkBuilder.network(for: .testnet)
var blockRepository: BlockRepository!
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
self.cacheDbURL = try! __cacheDbURL()
self.dataDbURL = try! __dataDbURL()
deleteDBs()
}
private func deleteDBs() {
try? FileManager.default.removeItem(at: cacheDbURL)
try? FileManager.default.removeItem(at: dataDbURL)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
try? FileManager.default.removeItem(at: cacheDbURL)
try? FileManager.default.removeItem(at: dataDbURL)
}
func testSingleDownloadAndScan() async throws {
logger = SampleLogger(logLevel: .debug)
XCTAssertNoThrow(try rustWelding.initDataDb(dbData: dataDbURL, seed: nil, networkType: network.networkType))
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let service = LightWalletGRPCService(
endpoint: LightWalletEndpoint(
address: "lightwalletd.testnet.electriccoin.co",
port: 9067
)
)
let blockCount = 100
let range = network.constants.saplingActivationHeight ... network.constants.saplingActivationHeight + blockCount
let downloader = CompactBlockDownloader.sqlDownloader(
service: service,
at: cacheDbURL
)!
let processorConfig = CompactBlockProcessor.Configuration.standard(
for: network,
walletBirthday: network.constants.saplingActivationHeight
)
let compactBlockProcessor = CompactBlockProcessor(
service: service,
storage: storage,
backend: ZcashRustBackend.self,
config: processorConfig
)
let repository = BlockSQLDAO(dbProvider: SimpleConnectionProvider.init(path: self.dataDbURL.absoluteString, readonly: true))
var latestScannedheight = BlockHeight.empty()
do {
try await compactBlockProcessor.compactBlockDownload(
downloader: downloader,
range: range
)
XCTAssertFalse(Task.isCancelled)
try compactBlockProcessor.compactBlockScanning(
rustWelding: rustWelding,
cacheDb: cacheDbURL,
dataDb: dataDbURL,
networkType: network.networkType
)
latestScannedheight = repository.lastScannedBlockHeight()
XCTAssertEqual(latestScannedheight, range.upperBound)
} catch {
XCTFail("Download failed with error: \(error)")
}
}
@objc func observeBenchmark(_ notification: Notification) {
guard let report = SDKMetrics.blockReportFromNotification(notification) else {
return
}
print("observed benchmark: \(report)")
}
func testScanValidateDownload() async throws {
logger = SampleLogger(logLevel: .debug)
NotificationCenter.default.addObserver(
self,
selector: #selector(observeBenchmark(_:)),
name: SDKMetrics.notificationName,
object: nil
)
guard try self.rustWelding.initDataDb(dbData: dataDbURL, seed: nil, networkType: network.networkType) == .success else {
XCTFail("Seed should not be required for this test")
return
}
let uvks = try DerivationTool(networkType: .testnet).deriveUnifiedFullViewingKeys(seed: TestSeed().seed(), numberOfAccounts: 1)
guard try self.rustWelding.initAccountsTable(dbData: self.dataDbURL, ufvks: uvks, networkType: network.networkType) else {
XCTFail("failed to init account table")
return
}
try self.rustWelding.initBlocksTable(
dbData: dataDbURL,
height: Int32(walletBirthDay.height),
hash: walletBirthDay.hash,
time: walletBirthDay.time,
saplingTree: walletBirthDay.saplingTree,
networkType: network.networkType
)
let service = LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.eccTestnet)
let storage = CompactBlockStorage(url: cacheDbURL, readonly: false)
try storage.createTable()
var processorConfig = CompactBlockProcessor.Configuration(
cacheDb: cacheDbURL,
dataDb: dataDbURL,
walletBirthday: network.constants.saplingActivationHeight,
network: network
)
processorConfig.scanningBatchSize = 1000
let compactBlockProcessor = CompactBlockProcessor(
service: service,
storage: storage,
backend: rustWelding,
config: processorConfig
)
let range = CompactBlockRange(
uncheckedBounds: (walletBirthDay.height, walletBirthDay.height + 10000)
)
do {
try await compactBlockProcessor.compactBlockStreamDownload(
blockBufferSize: 10,
startHeight: range.lowerBound,
targetHeight: range.upperBound
)
XCTAssertFalse(Task.isCancelled)
try await compactBlockProcessor.compactBlockValidation()
XCTAssertFalse(Task.isCancelled)
try await compactBlockProcessor.compactBlockBatchScanning(range: range)
XCTAssertFalse(Task.isCancelled)
} catch {
if let lwdError = error as? LightWalletServiceError {
switch lwdError {
case .timeOut:
XCTAssert(true)
default:
XCTFail("LWD Service error found, but should have been a timeLimit reached Error - \(lwdError)")
}
} else {
XCTFail("Error should have been a timeLimit reached Error - \(error)")
}
}
}
}

View File

@ -11,12 +11,6 @@ import XCTest
// swiftlint:disable print_function_usage // swiftlint:disable print_function_usage
class BlockStreamingTest: XCTestCase { class BlockStreamingTest: XCTestCase {
var queue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
override func setUpWithError() throws { override func setUpWithError() throws {
try super.setUpWithError() try super.setUpWithError()
logger = SampleLogger(logLevel: .debug) logger = SampleLogger(logLevel: .debug)
@ -27,7 +21,7 @@ class BlockStreamingTest: XCTestCase {
try? FileManager.default.removeItem(at: __dataDbURL()) try? FileManager.default.removeItem(at: __dataDbURL())
} }
func testStreamOperation() throws { func testStream() throws {
let expectation = XCTestExpectation(description: "blockstream expectation") let expectation = XCTestExpectation(description: "blockstream expectation")
let service = LightWalletGRPCService( let service = LightWalletGRPCService(
@ -61,9 +55,7 @@ class BlockStreamingTest: XCTestCase {
wait(for: [expectation], timeout: 1000) wait(for: [expectation], timeout: 1000)
} }
func testStreamOperationCancellation() throws { func testStreamCancellation() async throws {
let expectation = XCTestExpectation(description: "blockstream expectation")
let service = LightWalletGRPCService( let service = LightWalletGRPCService(
host: LightWalletEndpointBuilder.eccTestnet.host, host: LightWalletEndpointBuilder.eccTestnet.host,
port: 9067, port: 9067,
@ -71,37 +63,38 @@ class BlockStreamingTest: XCTestCase {
singleCallTimeout: 10000, singleCallTimeout: 10000,
streamingCallTimeout: 10000 streamingCallTimeout: 10000
) )
let storage = try TestDbBuilder.inMemoryCompactBlockStorage() let storage = try TestDbBuilder.inMemoryCompactBlockStorage()
let startHeight = try service.latestBlockHeight() - 100_000 let startHeight = try service.latestBlockHeight() - 100_000
let operation = CompactBlockStreamDownloadOperation( let processorConfig = CompactBlockProcessor.Configuration.standard(
for: ZcashNetworkBuilder.network(for: .testnet),
walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight
)
let compactBlockProcessor = CompactBlockProcessor(
service: service, service: service,
storage: storage, storage: storage,
blockBufferSize: 10, backend: ZcashRustBackend.self,
startHeight: startHeight, config: processorConfig
progressDelegate: self
) )
operation.completionHandler = { _, cancelled in let cancelableTask = Task {
XCTAssert(cancelled) do {
expectation.fulfill() try await compactBlockProcessor.compactBlockStreamDownload(
blockBufferSize: 10,
startHeight: startHeight
)
XCTAssertTrue(Task.isCancelled)
} catch {
XCTFail("failed with error: \(error)")
}
} }
operation.errorHandler = { error in try await Task.sleep(nanoseconds: 3_000_000_000)
XCTFail("failed with error: \(error)") cancelableTask.cancel()
expectation.fulfill()
}
queue.addOperation(operation)
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
self.queue.cancelAllOperations()
})
wait(for: [expectation], timeout: 1000)
} }
func testStreamOperationTimeout() throws { func testStreamTimeout() async throws {
let expectation = XCTestExpectation(description: "blockstream expectation")
let errorExpectation = XCTestExpectation(description: "blockstream error expectation")
let service = LightWalletGRPCService( let service = LightWalletGRPCService(
host: LightWalletEndpointBuilder.eccTestnet.host, host: LightWalletEndpointBuilder.eccTestnet.host,
port: 9067, port: 9067,
@ -109,49 +102,49 @@ class BlockStreamingTest: XCTestCase {
singleCallTimeout: 1000, singleCallTimeout: 1000,
streamingCallTimeout: 3000 streamingCallTimeout: 3000
) )
let storage = try TestDbBuilder.inMemoryCompactBlockStorage() let storage = try TestDbBuilder.inMemoryCompactBlockStorage()
let startHeight = try service.latestBlockHeight() - 100_000 let startHeight = try service.latestBlockHeight() - 100_000
let operation = CompactBlockStreamDownloadOperation(
let processorConfig = CompactBlockProcessor.Configuration.standard(
for: ZcashNetworkBuilder.network(for: .testnet),
walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight
)
let compactBlockProcessor = CompactBlockProcessor(
service: service, service: service,
storage: storage, storage: storage,
blockBufferSize: 10, backend: ZcashRustBackend.self,
startHeight: startHeight, config: processorConfig
progressDelegate: self
) )
operation.completionHandler = { finished, _ in let date = Date()
XCTAssert(finished)
expectation.fulfill()
}
operation.errorHandler = { error in do {
try await compactBlockProcessor.compactBlockStreamDownload(
blockBufferSize: 10,
startHeight: startHeight
)
} catch {
if let lwdError = error as? LightWalletServiceError { if let lwdError = error as? LightWalletServiceError {
switch lwdError { switch lwdError {
case .timeOut: case .timeOut:
XCTAssert(true) XCTAssert(true)
default: default:
XCTFail("LWD Service erro found, but should have been a timeLimit reached Error") XCTFail("LWD Service error found, but should have been a timeLimit reached Error")
} }
} else { } else {
XCTFail("Error should have been a timeLimit reached Error") XCTFail("Error should have been a timeLimit reached Error")
} }
errorExpectation.fulfill()
} }
queue.addOperation(operation)
let date = Date()
wait(for: [errorExpectation], timeout: 4)
let now = Date() let now = Date()
let elapsed = now.timeIntervalSince(date) let elapsed = now.timeIntervalSince(date)
print("took \(elapsed) seconds") print("took \(elapsed) seconds")
} }
func testBatchOperation() throws { func testBatch() async throws {
let expectation = XCTestExpectation(description: "blockbatch expectation")
let service = LightWalletGRPCService( let service = LightWalletGRPCService(
host: LightWalletEndpointBuilder.eccTestnet.host, host: LightWalletEndpointBuilder.eccTestnet.host,
port: 9067, port: 9067,
@ -162,34 +155,29 @@ class BlockStreamingTest: XCTestCase {
let storage = try TestDbBuilder.diskCompactBlockStorage(at: __dataDbURL() ) let storage = try TestDbBuilder.diskCompactBlockStorage(at: __dataDbURL() )
let targetHeight = try service.latestBlockHeight() let targetHeight = try service.latestBlockHeight()
let startHeight = targetHeight - 10_000 let startHeight = targetHeight - 10_000
let operation = CompactBlockBatchDownloadOperation(
let processorConfig = CompactBlockProcessor.Configuration.standard(
for: ZcashNetworkBuilder.network(for: .testnet),
walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight
)
let compactBlockProcessor = CompactBlockProcessor(
service: service, service: service,
storage: storage, storage: storage,
startHeight: startHeight, backend: ZcashRustBackend.self,
targetHeight: targetHeight, config: processorConfig
progressDelegate: self
) )
operation.completionHandler = { _, cancelled in let range = CompactBlockRange(uncheckedBounds: (startHeight, targetHeight))
if cancelled { do {
XCTFail("operation cancelled") try await compactBlockProcessor.compactBlockBatchDownload(range: range)
} XCTAssertFalse(Task.isCancelled)
expectation.fulfill() } catch {
}
operation.errorHandler = { error in
XCTFail("failed with error: \(error)") XCTFail("failed with error: \(error)")
expectation.fulfill()
} }
queue.addOperation(operation)
wait(for: [expectation], timeout: 120)
} }
func testBatchOperationCancellation() throws { func testBatchCancellation() async throws {
let expectation = XCTestExpectation(description: "blockbatch expectation")
let service = LightWalletGRPCService( let service = LightWalletGRPCService(
host: LightWalletEndpointBuilder.eccTestnet.host, host: LightWalletEndpointBuilder.eccTestnet.host,
port: 9067, port: 9067,
@ -200,36 +188,30 @@ class BlockStreamingTest: XCTestCase {
let storage = try TestDbBuilder.diskCompactBlockStorage(at: __dataDbURL() ) let storage = try TestDbBuilder.diskCompactBlockStorage(at: __dataDbURL() )
let targetHeight = try service.latestBlockHeight() let targetHeight = try service.latestBlockHeight()
let startHeight = targetHeight - 100_000 let startHeight = targetHeight - 100_000
let operation = CompactBlockBatchDownloadOperation(
let processorConfig = CompactBlockProcessor.Configuration.standard(
for: ZcashNetworkBuilder.network(for: .testnet),
walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight
)
let compactBlockProcessor = CompactBlockProcessor(
service: service, service: service,
storage: storage, storage: storage,
startHeight: startHeight, backend: ZcashRustBackend.self,
targetHeight: targetHeight, config: processorConfig
progressDelegate: self
) )
operation.completionHandler = { _, cancelled in let range = CompactBlockRange(uncheckedBounds: (startHeight, targetHeight))
XCTAssert(cancelled) let cancelableTask = Task {
expectation.fulfill() do {
try await compactBlockProcessor.compactBlockBatchDownload(range: range)
XCTAssertTrue(Task.isCancelled)
} catch {
XCTFail("failed with error: \(error)")
}
} }
operation.errorHandler = { error in try await Task.sleep(nanoseconds: 3_000_000_000)
XCTFail("failed with error: \(error)") cancelableTask.cancel()
expectation.fulfill()
}
queue.addOperation(operation)
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
self.queue.cancelAllOperations()
})
wait(for: [expectation], timeout: 1000)
}
}
extension BlockStreamingTest: CompactBlockProgressDelegate {
func progressUpdated(_ progress: CompactBlockProgress) {
print("progressHeight: \(String(describing: progress.progressHeight))")
print("startHeight: \(progress.progress)")
print("targetHeight: \(String(describing: progress.targetHeight))")
} }
} }

View File

@ -1,5 +1,5 @@
// //
// DownloadOperationTests.swift // DownloadTests.swift
// ZcashLightClientKitTests // ZcashLightClientKitTests
// //
// Created by Francisco Gindre on 10/16/19. // Created by Francisco Gindre on 10/16/19.
@ -12,40 +12,38 @@ import SQLite
@testable import ZcashLightClientKit @testable import ZcashLightClientKit
// swiftlint:disable force_try // swiftlint:disable force_try
class DownloadOperationTests: XCTestCase { class DownloadTests: XCTestCase {
var operationQueue = OperationQueue()
var network = ZcashNetworkBuilder.network(for: .testnet) var network = ZcashNetworkBuilder.network(for: .testnet)
override func tearDown() { override func tearDown() {
super.tearDown() super.tearDown()
operationQueue.cancelAllOperations()
} }
func testSingleOperation() { func testSingleDownload() async throws {
let expect = XCTestExpectation(description: self.description)
let service = LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.eccTestnet) let service = LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.eccTestnet)
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage() let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let downloader = CompactBlockDownloader(service: service, storage: storage) let downloader = CompactBlockDownloader(service: service, storage: storage)
let blockCount = 100 let blockCount = 100
let activationHeight = network.constants.saplingActivationHeight let activationHeight = network.constants.saplingActivationHeight
let range = activationHeight ... activationHeight + blockCount let range = activationHeight ... activationHeight + blockCount
let downloadOperation = CompactBlockDownloadOperation(downloader: downloader, range: range)
downloadOperation.completionHandler = { finished, cancelled in let processorConfig = CompactBlockProcessor.Configuration.standard(
expect.fulfill() for: network,
XCTAssertTrue(finished) walletBirthday: network.constants.saplingActivationHeight
XCTAssertFalse(cancelled) )
let compactBlockProcessor = CompactBlockProcessor(
service: service,
storage: storage,
backend: ZcashRustBackend.self,
config: processorConfig
)
do {
try await compactBlockProcessor.compactBlockDownload(downloader: downloader, range: range)
} catch {
XCTFail("Download failed with error: \(error)")
} }
downloadOperation.errorHandler = { error in
XCTFail("Donwload Operation failed with error: \(error)")
}
operationQueue.addOperation(downloadOperation)
wait(for: [expect], timeout: 10)
XCTAssertEqual(try! storage.latestHeight(), range.upperBound) XCTAssertEqual(try! storage.latestHeight(), range.upperBound)
} }
} }

View File

@ -11,29 +11,13 @@ import XCTest
// swiftlint:disable force_try type_body_length // swiftlint:disable force_try type_body_length
class BlockBatchValidationTests: XCTestCase { class BlockBatchValidationTests: XCTestCase {
var queue: OperationQueue = { func testBranchIdFailure() async throws {
let queue = OperationQueue()
queue.name = "Test Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testBranchIdFailure() throws {
let network = ZcashNetworkBuilder.network(for: .mainnet) let network = ZcashNetworkBuilder.network(for: .mainnet)
let service = MockLightWalletService( let service = MockLightWalletService(
latestBlockHeight: 1210000, latestBlockHeight: 1210000,
service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default) service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
) )
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
let downloader = CompactBlockDownloader(service: service, storage: repository) let downloader = CompactBlockDownloader(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration( let config = CompactBlockProcessor.Configuration(
@ -59,17 +43,18 @@ class BlockBatchValidationTests: XCTestCase {
let mockRust = MockRustBackend.self let mockRust = MockRustBackend.self
mockRust.consensusBranchID = Int32(0xd34d) mockRust.consensusBranchID = Int32(0xd34d)
let operation = FigureNextBatchOperation(downloader: downloader, service: service, config: config, rustBackend: mockRust) let compactBlockProcessor = CompactBlockProcessor(
let expectation = XCTestExpectation(description: "failure expectation") service: service,
let startedExpectation = XCTestExpectation(description: "start Expectation") storage: storage,
backend: mockRust,
operation.startedHandler = { config: config
startedExpectation.fulfill() )
}
do {
operation.errorHandler = { error in try await compactBlockProcessor.figureNextBatch(downloader: downloader)
expectation.fulfill() XCTAssertFalse(Task.isCancelled)
} catch {
switch error { switch error {
case CompactBlockProcessorError.wrongConsensusBranchId: case CompactBlockProcessorError.wrongConsensusBranchId:
break break
@ -77,19 +62,15 @@ class BlockBatchValidationTests: XCTestCase {
XCTFail("Expected CompactBlockProcessorError.wrongConsensusBranchId but found \(error)") XCTFail("Expected CompactBlockProcessorError.wrongConsensusBranchId but found \(error)")
} }
} }
queue.addOperations([operation], waitUntilFinished: false)
wait(for: [startedExpectation, expectation], timeout: 1, enforceOrder: true)
XCTAssertNotNil(operation.error)
XCTAssertTrue(operation.isCancelled)
} }
func testBranchNetworkMismatchFailure() throws { func testBranchNetworkMismatchFailure() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet) let network = ZcashNetworkBuilder.network(for: .mainnet)
let service = MockLightWalletService( let service = MockLightWalletService(
latestBlockHeight: 1210000, latestBlockHeight: 1210000,
service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default) service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
) )
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
let downloader = CompactBlockDownloader(service: service, storage: repository) let downloader = CompactBlockDownloader(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration( let config = CompactBlockProcessor.Configuration(
@ -115,17 +96,18 @@ class BlockBatchValidationTests: XCTestCase {
let mockRust = MockRustBackend.self let mockRust = MockRustBackend.self
mockRust.consensusBranchID = 0xd34db4d mockRust.consensusBranchID = 0xd34db4d
let operation = FigureNextBatchOperation(downloader: downloader, service: service, config: config, rustBackend: mockRust) let compactBlockProcessor = CompactBlockProcessor(
let expectation = XCTestExpectation(description: "failure expectation") service: service,
let startedExpectation = XCTestExpectation(description: "start Expectation") storage: storage,
backend: mockRust,
operation.startedHandler = { config: config
startedExpectation.fulfill() )
}
do {
operation.errorHandler = { error in try await compactBlockProcessor.figureNextBatch(downloader: downloader)
expectation.fulfill() XCTAssertFalse(Task.isCancelled)
} catch {
switch error { switch error {
case CompactBlockProcessorError.networkMismatch(expected: .mainnet, found: .testnet): case CompactBlockProcessorError.networkMismatch(expected: .mainnet, found: .testnet):
break break
@ -133,20 +115,15 @@ class BlockBatchValidationTests: XCTestCase {
XCTFail("Expected CompactBlockProcessorError.networkMismatch but found \(error)") XCTFail("Expected CompactBlockProcessorError.networkMismatch but found \(error)")
} }
} }
queue.addOperations([operation], waitUntilFinished: false)
wait(for: [startedExpectation, expectation], timeout: 1, enforceOrder: true)
XCTAssertNotNil(operation.error)
XCTAssertTrue(operation.isCancelled)
} }
func testBranchNetworkTypeWrongFailure() throws { func testBranchNetworkTypeWrongFailure() async throws {
let network = ZcashNetworkBuilder.network(for: .testnet) let network = ZcashNetworkBuilder.network(for: .testnet)
let service = MockLightWalletService( let service = MockLightWalletService(
latestBlockHeight: 1210000, latestBlockHeight: 1210000,
service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default) service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
) )
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
let downloader = CompactBlockDownloader(service: service, storage: repository) let downloader = CompactBlockDownloader(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration( let config = CompactBlockProcessor.Configuration(
@ -172,17 +149,18 @@ class BlockBatchValidationTests: XCTestCase {
let mockRust = MockRustBackend.self let mockRust = MockRustBackend.self
mockRust.consensusBranchID = 0xd34db4d mockRust.consensusBranchID = 0xd34db4d
let operation = FigureNextBatchOperation(downloader: downloader, service: service, config: config, rustBackend: mockRust) let compactBlockProcessor = CompactBlockProcessor(
let expectation = XCTestExpectation(description: "failure expectation") service: service,
let startedExpectation = XCTestExpectation(description: "start Expectation") storage: storage,
backend: mockRust,
operation.startedHandler = { config: config
startedExpectation.fulfill() )
}
do {
operation.errorHandler = { error in try await compactBlockProcessor.figureNextBatch(downloader: downloader)
expectation.fulfill() XCTAssertFalse(Task.isCancelled)
} catch {
switch error { switch error {
case CompactBlockProcessorError.generalError: case CompactBlockProcessorError.generalError:
break break
@ -190,20 +168,15 @@ class BlockBatchValidationTests: XCTestCase {
XCTFail("Expected CompactBlockProcessorError.generalError but found \(error)") XCTFail("Expected CompactBlockProcessorError.generalError but found \(error)")
} }
} }
queue.addOperations([operation], waitUntilFinished: false)
wait(for: [startedExpectation, expectation], timeout: 1, enforceOrder: true)
XCTAssertNotNil(operation.error)
XCTAssertTrue(operation.isCancelled)
} }
func testSaplingActivationHeightMismatch() throws { func testSaplingActivationHeightMismatch() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet) let network = ZcashNetworkBuilder.network(for: .mainnet)
let service = MockLightWalletService( let service = MockLightWalletService(
latestBlockHeight: 1210000, latestBlockHeight: 1210000,
service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default) service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
) )
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000) let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
let downloader = CompactBlockDownloader(service: service, storage: repository) let downloader = CompactBlockDownloader(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration( let config = CompactBlockProcessor.Configuration(
@ -230,17 +203,18 @@ class BlockBatchValidationTests: XCTestCase {
let mockRust = MockRustBackend.self let mockRust = MockRustBackend.self
mockRust.consensusBranchID = 0xd34db4d mockRust.consensusBranchID = 0xd34db4d
let operation = FigureNextBatchOperation(downloader: downloader, service: service, config: config, rustBackend: mockRust) let compactBlockProcessor = CompactBlockProcessor(
let expectation = XCTestExpectation(description: "failure expectation") service: service,
let startedExpectation = XCTestExpectation(description: "start Expectation") storage: storage,
backend: mockRust,
operation.startedHandler = { config: config
startedExpectation.fulfill() )
}
do {
operation.errorHandler = { error in try await compactBlockProcessor.figureNextBatch(downloader: downloader)
expectation.fulfill() XCTAssertFalse(Task.isCancelled)
} catch {
switch error { switch error {
case CompactBlockProcessorError.saplingActivationMismatch( case CompactBlockProcessorError.saplingActivationMismatch(
expected: network.constants.saplingActivationHeight, expected: network.constants.saplingActivationHeight,
@ -251,15 +225,9 @@ class BlockBatchValidationTests: XCTestCase {
XCTFail("Expected CompactBlockProcessorError.saplingActivationMismatch but found \(error)") XCTFail("Expected CompactBlockProcessorError.saplingActivationMismatch but found \(error)")
} }
} }
queue.addOperations([operation], waitUntilFinished: false)
wait(for: [startedExpectation, expectation], timeout: 1, enforceOrder: true)
XCTAssertNotNil(operation.error)
XCTAssertTrue(operation.isCancelled)
} }
func testResultIsWait() throws { func testResultIsWait() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet) let network = ZcashNetworkBuilder.network(for: .mainnet)
let expectedLatestHeight = BlockHeight(1210000) let expectedLatestHeight = BlockHeight(1210000)
@ -268,10 +236,11 @@ class BlockBatchValidationTests: XCTestCase {
service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default) service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
) )
let expectedStoreLatestHeight = BlockHeight(1220000) let expectedStoreLatestHeight = BlockHeight(1220000)
let expectedResult = FigureNextBatchOperation.NextState.wait( let expectedResult = CompactBlockProcessor.NextState.wait(
latestHeight: expectedLatestHeight, latestHeight: expectedLatestHeight,
latestDownloadHeight: expectedLatestHeight latestDownloadHeight: expectedLatestHeight
) )
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight) let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight)
let downloader = CompactBlockDownloader(service: service, storage: repository) let downloader = CompactBlockDownloader(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration( let config = CompactBlockProcessor.Configuration(
@ -298,50 +267,41 @@ class BlockBatchValidationTests: XCTestCase {
let mockRust = MockRustBackend.self let mockRust = MockRustBackend.self
mockRust.consensusBranchID = 0xd34db4d mockRust.consensusBranchID = 0xd34db4d
let operation = FigureNextBatchOperation(downloader: downloader, service: service, config: config, rustBackend: mockRust) let compactBlockProcessor = CompactBlockProcessor(
let completedExpectation = XCTestExpectation(description: "completed expectation") service: service,
let startedExpectation = XCTestExpectation(description: "start Expectation") storage: storage,
backend: mockRust,
operation.startedHandler = { config: config
startedExpectation.fulfill() )
}
var nextBatch: CompactBlockProcessor.NextState?
operation.errorHandler = { error in do {
nextBatch = try await compactBlockProcessor.figureNextBatch(downloader: downloader)
XCTAssertFalse(Task.isCancelled)
} catch {
XCTFail("this shouldn't happen: \(error)") XCTFail("this shouldn't happen: \(error)")
} }
operation.completionHandler = { finished, cancelled in
completedExpectation.fulfill()
XCTAssertTrue(finished)
XCTAssertFalse(cancelled)
}
queue.addOperations([operation], waitUntilFinished: false)
wait(for: [startedExpectation, completedExpectation], timeout: 1, enforceOrder: true) guard let _ = nextBatch else {
XCTAssertNil(operation.error)
XCTAssertFalse(operation.isCancelled)
guard let result = operation.result else {
XCTFail("result should not be nil") XCTFail("result should not be nil")
return return
} }
XCTAssertTrue( XCTAssertTrue(
{ {
switch result { switch nextBatch {
case .wait(latestHeight: expectedLatestHeight, latestDownloadHeight: expectedLatestHeight): case .wait(latestHeight: expectedLatestHeight, latestDownloadHeight: expectedLatestHeight):
return true return true
default: default:
return false return false
} }
}(), }(),
"Expected \(expectedResult) got: \(result)" "Expected \(expectedResult) got: \(String(describing: nextBatch))"
) )
} }
func testResultProcessNew() throws { func testResultProcessNew() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet) let network = ZcashNetworkBuilder.network(for: .mainnet)
let expectedLatestHeight = BlockHeight(1230000) let expectedLatestHeight = BlockHeight(1230000)
let service = MockLightWalletService( let service = MockLightWalletService(
@ -350,13 +310,14 @@ class BlockBatchValidationTests: XCTestCase {
) )
let expectedStoreLatestHeight = BlockHeight(1220000) let expectedStoreLatestHeight = BlockHeight(1220000)
let walletBirthday = BlockHeight(1210000) let walletBirthday = BlockHeight(1210000)
let expectedResult = FigureNextBatchOperation.NextState.processNewBlocks( let expectedResult = CompactBlockProcessor.NextState.processNewBlocks(
range: CompactBlockProcessor.nextBatchBlockRange( range: CompactBlockProcessor.nextBatchBlockRange(
latestHeight: expectedLatestHeight, latestHeight: expectedLatestHeight,
latestDownloadedHeight: expectedStoreLatestHeight, latestDownloadedHeight: expectedStoreLatestHeight,
walletBirthday: walletBirthday walletBirthday: walletBirthday
) )
) )
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight) let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight)
let downloader = CompactBlockDownloader(service: service, storage: repository) let downloader = CompactBlockDownloader(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration( let config = CompactBlockProcessor.Configuration(
@ -383,50 +344,41 @@ class BlockBatchValidationTests: XCTestCase {
let mockRust = MockRustBackend.self let mockRust = MockRustBackend.self
mockRust.consensusBranchID = 0xd34db4d mockRust.consensusBranchID = 0xd34db4d
let operation = FigureNextBatchOperation(downloader: downloader, service: service, config: config, rustBackend: mockRust)
let completedExpectation = XCTestExpectation(description: "completed expectation")
let startedExpectation = XCTestExpectation(description: "start Expectation")
operation.startedHandler = {
startedExpectation.fulfill()
}
operation.errorHandler = { _ in
XCTFail("this shouldn't happen")
}
operation.completionHandler = { finished, cancelled in
completedExpectation.fulfill()
XCTAssertTrue(finished)
XCTAssertFalse(cancelled)
}
queue.addOperations([operation], waitUntilFinished: false)
wait(for: [startedExpectation, completedExpectation], timeout: 1, enforceOrder: true) let compactBlockProcessor = CompactBlockProcessor(
XCTAssertNil(operation.error) service: service,
XCTAssertFalse(operation.isCancelled) storage: storage,
backend: mockRust,
guard let result = operation.result else { config: config
)
var nextBatch: CompactBlockProcessor.NextState?
do {
nextBatch = try await compactBlockProcessor.figureNextBatch(downloader: downloader)
XCTAssertFalse(Task.isCancelled)
} catch {
XCTFail("this shouldn't happen: \(error)")
}
guard let _ = nextBatch else {
XCTFail("result should not be nil") XCTFail("result should not be nil")
return return
} }
XCTAssertTrue( XCTAssertTrue(
{ {
switch result { switch nextBatch {
case .processNewBlocks(range: CompactBlockRange(uncheckedBounds: (expectedStoreLatestHeight + 1, expectedLatestHeight))): case .processNewBlocks(range: CompactBlockRange(uncheckedBounds: (expectedStoreLatestHeight + 1, expectedLatestHeight))):
return true return true
default: default:
return false return false
} }
}(), }(),
"Expected \(expectedResult) got: \(result)" "Expected \(expectedResult) got: \(String(describing: nextBatch))"
) )
} }
func testResultProcessorFinished() throws { func testResultProcessorFinished() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet) let network = ZcashNetworkBuilder.network(for: .mainnet)
let expectedLatestHeight = BlockHeight(1230000) let expectedLatestHeight = BlockHeight(1230000)
let service = MockLightWalletService( let service = MockLightWalletService(
@ -435,7 +387,8 @@ class BlockBatchValidationTests: XCTestCase {
) )
let expectedStoreLatestHeight = BlockHeight(1230000) let expectedStoreLatestHeight = BlockHeight(1230000)
let walletBirthday = BlockHeight(1210000) let walletBirthday = BlockHeight(1210000)
let expectedResult = FigureNextBatchOperation.NextState.finishProcessing(height: expectedStoreLatestHeight) let expectedResult = CompactBlockProcessor.NextState.finishProcessing(height: expectedStoreLatestHeight)
let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight) let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight)
let downloader = CompactBlockDownloader(service: service, storage: repository) let downloader = CompactBlockDownloader(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration( let config = CompactBlockProcessor.Configuration(
@ -462,46 +415,38 @@ class BlockBatchValidationTests: XCTestCase {
let mockRust = MockRustBackend.self let mockRust = MockRustBackend.self
mockRust.consensusBranchID = 0xd34db4d mockRust.consensusBranchID = 0xd34db4d
let operation = FigureNextBatchOperation(downloader: downloader, service: service, config: config, rustBackend: mockRust)
let completedExpectation = XCTestExpectation(description: "completed expectation")
let startedExpectation = XCTestExpectation(description: "start Expectation")
operation.startedHandler = {
startedExpectation.fulfill()
}
operation.errorHandler = { _ in
XCTFail("this shouldn't happen")
}
operation.completionHandler = { finished, cancelled in
completedExpectation.fulfill()
XCTAssertTrue(finished)
XCTAssertFalse(cancelled)
}
queue.addOperations([operation], waitUntilFinished: false)
wait(for: [startedExpectation, completedExpectation], timeout: 1, enforceOrder: true) let compactBlockProcessor = CompactBlockProcessor(
XCTAssertNil(operation.error) service: service,
XCTAssertFalse(operation.isCancelled) storage: storage,
backend: mockRust,
guard let result = operation.result else { config: config
)
var nextBatch: CompactBlockProcessor.NextState?
do {
nextBatch = try await compactBlockProcessor.figureNextBatch(downloader: downloader)
XCTAssertFalse(Task.isCancelled)
} catch {
XCTFail("this shouldn't happen: \(error)")
}
guard let _ = nextBatch else {
XCTFail("result should not be nil") XCTFail("result should not be nil")
return return
} }
XCTAssertTrue( XCTAssertTrue(
{ {
switch result {
switch nextBatch {
case .finishProcessing(height: expectedLatestHeight): case .finishProcessing(height: expectedLatestHeight):
return true return true
default: default:
return false return false
} }
}(), }(),
"Expected \(expectedResult) got: \(result)" "Expected \(expectedResult) got: \(String(describing: nextBatch))"
) )
} }
} }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'ZcashLightClientKit' s.name = 'ZcashLightClientKit'
s.version = '0.16.8-beta' s.version = '0.16.9-beta'
s.summary = 'Zcash Light Client wallet SDK for iOS' s.summary = 'Zcash Light Client wallet SDK for iOS'
s.description = <<-DESC s.description = <<-DESC

View File

@ -1,3 +1,16 @@
# 0.16.9-beta
Checkpoints added:
Mainnet
````
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1787500.json
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1790000.json
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1792500.json
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1795000.json
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1797500.json
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1800000.json
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1802500.json
Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1805000.json
````
# 0.16.8-beta # 0.16.8-beta
Checkpoints added: Checkpoints added:
Mainnet Mainnet