Merge branch 'master' into release/0.16.10-beta

This commit is contained in:
Francisco Gindre 2022-09-16 18:04:14 -03:00
commit db15465ebe
42 changed files with 2810 additions and 2578 deletions

View File

@ -17,7 +17,7 @@ jobs:
permissions: permissions:
contents: read contents: read
runs-on: macos-latest runs-on: macos-12
steps: steps:
- uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846

View File

@ -1,6 +1,6 @@
language: swift language: swift
os: osx os: osx
osx_image: xcode13.2 osx_image: xcode13.4
xcode_project: ./Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj xcode_project: ./Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj
xcode_scheme: ZcashLightClientSample xcode_scheme: ZcashLightClientSample
xcode_destination: platform=iOS Simulator,OS=15.2,name=iPhone 8 xcode_destination: platform=iOS Simulator,OS=15.2,name=iPhone 8

View File

@ -30,7 +30,7 @@ enum DemoAppConfig {
}() }()
static var endpoint: LightWalletEndpoint { static var endpoint: LightWalletEndpoint {
return LightWalletEndpoint(address: self.host, port: self.port, secure: true) return LightWalletEndpoint(address: self.host, port: self.port, secure: true, streamingCallTimeoutInMillis: 10 * 60 * 60 * 1000)
} }
} }

View File

@ -28,18 +28,13 @@ class LatestHeightViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
service.latestBlockHeight { result in /// Note: It's safe to modify model or call fail() because all methods of a UIViewController are MainActor methods by default.
switch result { Task {
case .success(let height): do {
DispatchQueue.main.async { [weak self] in model = try await service.latestBlockHeightAsync()
self?.model = height } catch {
} fail(error as? LightWalletServiceError ?? .unknown)
case .failure(let error):
DispatchQueue.main.async { [weak self] in
self?.fail(error)
}
} }
} }
} }

View File

@ -31,7 +31,7 @@ class SyncBlocksViewController: UIViewController {
// swiftlint:disable:next force_try // swiftlint:disable:next force_try
try! wallet.initialize() try! wallet.initialize()
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

@ -1,11 +1,11 @@
// swift-tools-version:5.5 // swift-tools-version:5.6
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "ZcashLightClientKit", name: "ZcashLightClientKit",
platforms: [ platforms: [
.iOS(.v13), .iOS(.v13),
.macOS(.v10_12) .macOS(.v10_15)
], ],
products: [ products: [
.library( .library(
@ -16,7 +16,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.8.0"), .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.8.0"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0"),
.package(name:"libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi.git", from: "0.0.3"), .package(name: "libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi.git", from: "0.0.3"),
], ],
targets: [ targets: [
.target( .target(

View File

@ -85,39 +85,28 @@ extension CompactBlockStorage: CompactBlockRepository {
try latestBlockHeight() try latestBlockHeight()
} }
func latestHeight(result: @escaping (Swift.Result<BlockHeight, Error>) -> Void) { func latestHeightAsync() async throws -> BlockHeight {
DispatchQueue.global(qos: .userInitiated).async { let task = Task(priority: .userInitiated) {
do { try latestBlockHeight()
result(.success(try self.latestBlockHeight()))
} catch {
result(.failure(error))
}
} }
return try await task.value
} }
func write(blocks: [ZcashCompactBlock]) throws { func write(blocks: [ZcashCompactBlock]) throws {
try insert(blocks) try insert(blocks)
} }
func write(blocks: [ZcashCompactBlock], completion: ((Error?) -> Void)?) { func writeAsync(blocks: [ZcashCompactBlock]) async throws {
DispatchQueue.global(qos: .userInitiated).async { let task = Task(priority: .userInitiated) {
do { try insert(blocks)
try self.insert(blocks)
completion?(nil)
} catch {
completion?(error)
}
} }
try await task.value
} }
func rewind(to height: BlockHeight, completion: ((Error?) -> Void)?) { func rewindAsync(to height: BlockHeight) async throws {
DispatchQueue.global(qos: .userInitiated).async { let task = Task(priority: .userInitiated) {
do { try rewind(to: height)
try self.rewind(to: height)
completion?(nil)
} catch {
completion?(error)
}
} }
try await task.value
} }
} }

View File

@ -16,59 +16,55 @@ enum CompactBlockDownloadError: Error {
Represents what a compact block downloaded should provide to its clients Represents what a compact block downloaded should provide to its clients
*/ */
public protocol CompactBlockDownloading { public protocol CompactBlockDownloading {
/**
Downloads and stores the given block range.
Non-Blocking
*/
func downloadBlockRange(
_ heightRange: CompactBlockRange,
completion: @escaping (Error?) -> Void
)
/**
Remove newer blocks and go back to the given height
- Parameters:
- height: the given height to rewind to
- completion: block to be executed after completing rewind
*/
func rewind(to height: BlockHeight, completion: @escaping (Error?) -> Void)
/**
returns the height of the latest compact block stored locally
BlockHeight.empty() if no blocks are stored yet
non-blocking
*/
func lastDownloadedBlockHeight(result: @escaping (Result<BlockHeight, Error>) -> Void)
/**
Returns the last height on the blockchain
Non-blocking
*/
func latestBlockHeight(result: @escaping (Result<BlockHeight, Error>) -> Void)
/** /**
Downloads and stores the given block range. Downloads and stores the given block range.
Blocking Blocking
*/ */
func downloadBlockRange(_ range: CompactBlockRange) throws func downloadBlockRange(_ range: CompactBlockRange) throws
/**
Downloads and stores the given block range.
Non-Blocking
*/
func downloadBlockRangeAsync(_ heightRange: CompactBlockRange) async throws
/** /**
Restore the download progress up to the given height. Restore the download progress up to the given height.
*/ */
func rewind(to height: BlockHeight) throws func rewind(to height: BlockHeight) throws
/**
Remove newer blocks and go back to the given height
- Parameter height: the given height to rewind to
*/
func rewindAsync(to height: BlockHeight) async throws
/** /**
Returns the height of the latest compact block stored locally. Returns the height of the latest compact block stored locally.
BlockHeight.empty() if no blocks are stored yet BlockHeight.empty() if no blocks are stored yet
Blocking Blocking
*/ */
func lastDownloadedBlockHeight() throws -> BlockHeight func lastDownloadedBlockHeight() throws -> BlockHeight
/**
returns the height of the latest compact block stored locally
BlockHeight.empty() if no blocks are stored yet
non-blocking
*/
func lastDownloadedBlockHeightAsync() async throws -> BlockHeight
/** /**
Returns the latest block height Returns the latest block height
Blocking Blocking
*/ */
func latestBlockHeight() throws -> BlockHeight func latestBlockHeight() throws -> BlockHeight
/**
Returns the last height on the blockchain
Non-blocking
*/
func latestBlockHeightAsync() async throws -> BlockHeight
/** /**
Gets the transaction for the Id given Gets the transaction for the Id given
@ -81,17 +77,30 @@ public protocol CompactBlockDownloading {
/** /**
Gets the transaction for the Id given Gets the transaction for the Id given
- Parameter txId: Data representing the transaction Id - Parameter txId: Data representing the transaction Id
- Parameter result: a handler for the result of the operation
*/ */
func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, Error>) -> Void) func fetchTransactionAsync(txId: Data) async throws -> TransactionEntity
func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) throws -> [UnspentTransactionOutputEntity] func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) throws -> [UnspentTransactionOutputEntity]
// TODO: will be removed with the issue 474
// https://github.com/zcash/ZcashLightClientKit/issues/474
// Use the new API fetchUnspentTransactionOutputs(...) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight, result: @escaping (Result<[UnspentTransactionOutputEntity], Error>) -> Void) func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight, result: @escaping (Result<[UnspentTransactionOutputEntity], Error>) -> Void)
func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
func fetchUnspentTransactionOutputs(tAddresses: [String], startHeight: BlockHeight) throws -> [UnspentTransactionOutputEntity] func fetchUnspentTransactionOutputs(tAddresses: [String], startHeight: BlockHeight) throws -> [UnspentTransactionOutputEntity]
func fetchUnspentTransactionOutputs(tAddresses: [String], startHeight: BlockHeight, result: @escaping (Result<[UnspentTransactionOutputEntity], Error>) -> Void) // TODO: will be removed with the issue 474
// https://github.com/zcash/ZcashLightClientKit/issues/474
// Use the new API fetchUnspentTransactionOutputs(...) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
func fetchUnspentTransactionOutputs(
tAddresses: [String],
startHeight: BlockHeight,
result: @escaping (Result<[UnspentTransactionOutputEntity], Error>) -> Void
)
func fetchUnspentTransactionOutputs(tAddresses: [String], startHeight: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
func closeConnection() func closeConnection()
} }
@ -137,6 +146,10 @@ extension CompactBlockDownloader: CompactBlockDownloading {
} }
} }
} }
func fetchUnspentTransactionOutputs(tAddresses: [String], startHeight: BlockHeight ) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
lightwalletService.fetchUTXOs(for: tAddresses, height: startHeight)
}
func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) throws -> [UnspentTransactionOutputEntity] { func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
try lightwalletService.fetchUTXOs(for: tAddress, height: startHeight) try lightwalletService.fetchUTXOs(for: tAddress, height: startHeight)
@ -153,68 +166,54 @@ extension CompactBlockDownloader: CompactBlockDownloading {
} }
} }
func latestBlockHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) { func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
lightwalletService.latestBlockHeight { fetchResult in lightwalletService.fetchUTXOs(for: tAddress, height: startHeight)
switch fetchResult { }
case .failure(let error):
result(.failure(error)) func latestBlockHeightAsync() async throws -> BlockHeight {
case .success(let height): try await lightwalletService.latestBlockHeightAsync()
result(.success(height))
}
}
} }
func latestBlockHeight() throws -> BlockHeight { func latestBlockHeight() throws -> BlockHeight {
try lightwalletService.latestBlockHeight() try lightwalletService.latestBlockHeight()
} }
/**
Downloads and stores the given block range.
Non-Blocking
*/
func downloadBlockRange(
_ heightRange: CompactBlockRange,
completion: @escaping (Error?) -> Void
) {
lightwalletService.blockRange(heightRange) { [weak self] result in
guard let self = self else {
return
}
switch result {
case .failure(let error):
completion(error)
case .success(let compactBlocks):
self.storage.write(blocks: compactBlocks) { storeError in
completion(storeError)
}
}
}
}
func downloadBlockRange(_ range: CompactBlockRange) throws { func downloadBlockRange(_ range: CompactBlockRange) throws {
let blocks = try lightwalletService.blockRange(range) let blocks = try lightwalletService.blockRange(range)
try storage.write(blocks: blocks) try storage.write(blocks: blocks)
} }
func rewind(to height: BlockHeight, completion: @escaping (Error?) -> Void) { func downloadBlockRangeAsync( _ heightRange: CompactBlockRange) async throws {
storage.rewind(to: height) { e in let stream: AsyncThrowingStream<ZcashCompactBlock, Error> = lightwalletService.blockRange(heightRange)
completion(e) do {
} var compactBlocks: [ZcashCompactBlock] = []
} for try await compactBlock in stream {
compactBlocks.append(compactBlock)
func lastDownloadedBlockHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) {
storage.latestHeight { heightResult in
switch heightResult {
case .failure(let e):
result(.failure(CompactBlockDownloadError.generalError(error: e)))
return
case .success(let height):
result(.success(height))
} }
try await self.storage.writeAsync(blocks: compactBlocks)
} catch {
throw error
} }
} }
func rewindAsync(to height: BlockHeight) async throws {
do {
try await storage.rewindAsync(to: height)
} catch {
throw error
}
}
func lastDownloadedBlockHeightAsync() async throws -> BlockHeight {
do {
let latestHeight = try await storage.latestHeightAsync()
return latestHeight
} catch {
throw CompactBlockDownloadError.generalError(error: error)
}
}
func rewind(to height: BlockHeight) throws { func rewind(to height: BlockHeight) throws {
try self.storage.rewind(to: height) try self.storage.rewind(to: height)
} }
@ -227,14 +226,7 @@ extension CompactBlockDownloader: CompactBlockDownloading {
try lightwalletService.fetchTransaction(txId: txId) try lightwalletService.fetchTransaction(txId: txId)
} }
func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, Error>) -> Void) { func fetchTransactionAsync(txId: Data) async throws -> TransactionEntity {
lightwalletService.fetchTransaction(txId: txId) { txResult in try await lightwalletService.fetchTransactionAsync(txId: txId)
switch txResult {
case .failure(let error):
result(.failure(error))
case .success(let transaction):
result(.success(transaction))
}
}
} }
} }

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,312 +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
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?()
do {
try downloader.downloadBlockRange(range)
} catch {
self.error = error
self.fail()
}
}
}
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 cancelable: CancellableCall?
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"
}
// swiftlint:disable cyclomatic_complexity
override func main() {
guard !shouldCancel() else {
cancel()
return
}
self.startedHandler?()
do {
if self.targetHeight == nil {
self.targetHeight = try service.latestBlockHeight()
}
guard let latestHeight = self.targetHeight else {
throw LightWalletServiceError.generalError(message: "missing target height on block stream operation")
}
let latestDownloaded = try storage.latestHeight()
let startHeight = max(self.startHeight ?? BlockHeight.empty(), latestDownloaded)
self.cancelable = self.service.blockStream(startHeight: startHeight, endHeight: latestHeight) { [weak self] blockResult in
guard let self = self, !self.isCancelled else {
self?.cancel()
return
}
switch blockResult {
case .success(let result):
switch result {
case .success:
do {
try self.flush()
self.done = true
} catch {
self.fail(error: error)
}
return
case .error(let e):
self.fail(error: e)
}
case .failure(let e):
if case .userCancelled = e {
self.done = true
} else {
self.fail(error: e)
}
}
} handler: {[weak self] block in
guard let self = self else { return }
guard !self.isCancelled else {
self.cancel()
return
}
do {
try self.cache(block, flushCache: false)
} catch {
self.fail(error: error)
}
} progress: { progress in
guard !self.isCancelled else {
self.cancel()
return
}
self.progressDelegate?.progressUpdated(.download(progress))
}
while !done && !isCancelled {
sleep(1)
}
if isCancelled {
self.cancel()
}
} catch {
self.fail(error: error)
}
}
override func fail(error: Error? = nil) {
self.cancelable?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelable?.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 maxRetries: Int
private var storage: CompactBlockStorage
private var service: LightWalletService
private var cancelable: CancellableCall?
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?()
do {
let localDownloadedHeight = try self.storage.latestHeight()
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 blocks = try service.blockRange(range)
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
}
} catch {
self.fail(error: error)
}
}
override func fail(error: Error? = nil) {
self.cancelable?.cancel()
super.fail(error: error)
}
override func cancel() {
self.cancelable?.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,157 +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
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?()
// 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 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.error = error
self.fail()
return
}
if let handler = self.txFoundHandler, let foundTxs = try? repository.findConfirmedTransactions(in: self.range, offset: 0, limit: Int.max) {
handler(foundTxs, self.range)
}
}
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) throws -> ConfirmedTransactionEntity {
LoggerProxy.debug("Zoom.... Enhance... Tx: \(transaction.transactionId.toHexStringTxId())")
let transaction = try downloader.fetchTransaction(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
}
}
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")
@ -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
@ -973,46 +775,42 @@ public class CompactBlockProcessor {
} }
private func nextBatch() { private func nextBatch() {
self.state = .downloading setState(.downloading)
NextStateHelper.nextState( Task { @MainActor [self] in
service: self.service, do {
downloader: self.downloader, let nextState = try await NextStateHelper.nextStateAsync(
config: self.config, service: self.service,
rustBackend: self.rustBackend, downloader: self.downloader,
queue: nil config: self.config,
) { result in rustBackend: self.rustBackend
DispatchQueue.main.async { [weak self] in )
guard let self = self else { return } switch nextState {
switch result { case .finishProcessing(let height):
case .success(let nextState): self.latestBlockHeight = height
switch nextState { self.processingFinished(height: height)
case .finishProcessing(let height): case .processNewBlocks(let range):
self.latestBlockHeight = height self.latestBlockHeight = range.upperBound
self.processingFinished(height: height) self.lowerBoundHeight = range.lowerBound
case .processNewBlocks(let range): self.processNewBlocks(range: range)
self.latestBlockHeight = range.upperBound case let .wait(latestHeight, latestDownloadHeight):
self.lowerBoundHeight = range.lowerBound // Lightwalletd might be syncing
self.processNewBlocks(range: range) self.lowerBoundHeight = latestDownloadHeight
case let .wait(latestHeight, latestDownloadHeight): self.latestBlockHeight = latestHeight
// Lightwalletd might be syncing LoggerProxy.info(
self.lowerBoundHeight = latestDownloadHeight "Lightwalletd might be syncing: latest downloaded block height is: \(latestDownloadHeight)" +
self.latestBlockHeight = latestHeight "while latest blockheight is reported at: \(latestHeight)"
LoggerProxy.info( )
"Lightwalletd might be syncing: latest downloaded block height is: \(latestDownloadHeight)" + self.processingFinished(height: latestDownloadHeight)
"while latest blockheight is reported at: \(latestHeight)"
)
self.processingFinished(height: latestDownloadHeight)
}
case .failure(let error):
self.severeFailure(error)
} }
} catch {
self.severeFailure(error)
} }
} }
} }
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
@ -1049,7 +847,7 @@ public class CompactBlockProcessor {
} }
} }
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
@ -1075,7 +873,7 @@ public class CompactBlockProcessor {
CompactBlockProcessorNotificationKey.foundBlocks: self.foundBlocks CompactBlockProcessorNotificationKey.foundBlocks: self.foundBlocks
] ]
) )
self.state = .synced setState(.synced)
setTimer() setTimer()
} }
@ -1262,33 +1060,27 @@ extension UnifiedAddressShim: UnifiedAddress {
} }
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)
} }
} }
@ -1351,6 +1143,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."
} }
} }
@ -1370,12 +1163,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(
@ -1388,30 +1175,23 @@ extension CompactBlockProcessor: EnhancementStreamDelegate {
extension CompactBlockProcessor { extension CompactBlockProcessor {
enum NextStateHelper { enum NextStateHelper {
// swiftlint:disable:next function_parameter_count static func nextStateAsync(
static func nextState(
service: LightWalletService, service: LightWalletService,
downloader: CompactBlockDownloading, downloader: CompactBlockDownloading,
config: Configuration, config: Configuration,
rustBackend: ZcashRustBackendWelding.Type, rustBackend: ZcashRustBackendWelding.Type
queue: DispatchQueue?, ) async throws -> NextState {
result: @escaping (Result<FigureNextBatchOperation.NextState, Error>) -> Void let task = Task(priority: .userInitiated) {
) { // TODO: refactor to async call, issue 463, PR 493
let dispatchQueue = queue ?? DispatchQueue.global(qos: .userInitiated) // https://github.com/zcash/ZcashLightClientKit/issues/463
try nextState(
dispatchQueue.async { service: service,
do { downloader: downloader,
let nextResult = try self.nextState( config: config,
service: service, rustBackend: rustBackend
downloader: downloader, )
config: config,
rustBackend: rustBackend
)
result(.success(nextResult))
} catch {
result(.failure(error))
}
} }
return try await task.value
} }
static func nextState( static func nextState(
@ -1419,7 +1199,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

@ -1,5 +1,5 @@
// //
// CompactBlockProcessingOperation.swift // CompactBlockProcessing.swift
// ZcashLightClientKit // ZcashLightClientKit
// //
// Created by Francisco Gindre on 10/15/19. // Created by Francisco Gindre on 10/15/19.
@ -8,37 +8,106 @@
import Foundation import Foundation
class CompactBlockScanningOperation: ZcashOperation { extension CompactBlockProcessor {
override var isConcurrent: Bool { false } func compactBlockBatchScanning(range: CompactBlockRange) async throws {
try Task.checkCancellation()
override var isAsynchronous: Bool { false }
setState(.scanning)
var rustBackend: ZcashRustBackendWelding.Type let batchSize = UInt32(config.scanningBatchSize)
private var cacheDb: URL do {
private var dataDb: URL if batchSize == 0 {
private var limit: UInt32 let scanStartTime = Date()
private var network: NetworkType guard self.rustBackend.scanBlocks(dbCache: config.cacheDb, dbData: config.dataDb, limit: batchSize, networkType: config.network.networkType) else {
init(rustWelding: ZcashRustBackendWelding.Type, cacheDb: URL, dataDb: URL, limit: UInt32 = 0, networkType: NetworkType) { let error: Error = rustBackend.lastError() ?? CompactBlockProcessorError.unknown
rustBackend = rustWelding LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
self.cacheDb = cacheDb throw error
self.dataDb = dataDb }
self.limit = limit let scanFinishTime = Date()
self.network = networkType NotificationCenter.default.post(
super.init() SDKMetrics.progressReportNotification(
} progress: BlockProgress(
startHeight: range.lowerBound,
override func main() { targetHeight: range.upperBound,
guard !shouldCancel() else { progressHeight: range.upperBound
cancel() ),
return 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
} }
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))") extension CompactBlockProcessor {
self.fail() func compactBlockScanning(
return 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
} }
} }
} }
@ -112,127 +181,3 @@ extension String.StringInterpolation {
appendLiteral(literal) 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 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?()
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
}
} catch {
scanFailed(error)
}
}
func scanFailed(_ error: Error) {
self.error = error
LoggerProxy.debug("block scanning failed with error: \(String(describing: self.error))")
self.fail()
}
}

View File

@ -8,59 +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
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)
let result = self.rustBackend.validateCombinedChain(dbCache: cacheDb, dbData: dataDb, networkType: self.network) let result = rustBackend.validateCombinedChain(dbCache: config.cacheDb, dbData: config.dataDb, networkType: config.network.networkType)
switch result { do {
case 0: switch result {
let error = CompactBlockValidationError.failedWithError(rustBackend.lastError()) case 0:
self.error = error let error = CompactBlockValidationError.failedWithError(rustBackend.lastError())
LoggerProxy.debug("block scanning failed with error: \(String(describing: self.error))") LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
self.fail(error: error) throw error
case ZcashRustBackendWeldingConstants.validChain:
if Task.isCancelled {
setState(.stopped)
LoggerProxy.debug("Warning: compactBlockValidation cancelled")
}
LoggerProxy.debug("validateChainFinished")
break
default:
let error = CompactBlockValidationError.validationFailed(height: BlockHeight(result))
LoggerProxy.debug("block scanning failed with error: \(String(describing: error))")
throw error
}
} catch {
guard let validationError = error as? CompactBlockValidationError else {
LoggerProxy.error("Warning: compactBlockValidation returning generic error: \(error)")
return
}
case ZcashRustBackendWeldingConstants.validChain: switch validationError {
break case .validationFailed(let height):
LoggerProxy.debug("chain validation at height: \(height)")
default: validationFailed(at: height)
let error = CompactBlockValidationError.validationFailed(height: BlockHeight(result)) case .failedWithError(let err):
self.error = error guard let validationFailure = err else {
LoggerProxy.debug("block scanning failed with error: \(String(describing: self.error))") LoggerProxy.error("validation failed without a specific error")
self.fail(error: error) self.fail(CompactBlockProcessorError.generalError(message: "validation failed without a specific error"))
return
}
throw validationFailure
}
} }
} }
} }

View File

@ -0,0 +1,82 @@
//
// 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,
address: utxo.address,
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,103 +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
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?()
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)
}
let utxos = try downloader.fetchUnspentTransactionOutputs(tAddresses: tAddresses, startHeight: startHeight)
let result = storeUTXOs(utxos, in: dataDb)
self.fetchedUTXOsHandler?(result)
} catch {
self.fail(error: error)
}
}
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,
address: utxo.address,
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)
}
}

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

@ -28,11 +28,9 @@ protocol CompactBlockRepository {
/** /**
Gets the highest block that is currently stored. Gets the highest block that is currently stored.
Non-Blocking Non-Blocking
- Parameter result: closure resulting on either the latest height or an error
*/ */
func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) func latestHeightAsync() async throws -> BlockHeight
/** /**
Write the given blocks to this store, which may be anything from an in-memory cache to a DB. Write the given blocks to this store, which may be anything from an in-memory cache to a DB.
Blocking Blocking
@ -46,10 +44,9 @@ protocol CompactBlockRepository {
Non-Blocking Non-Blocking
- Parameters: - Parameters:
- Parameter blocks: array of blocks to be written to storage - Parameter blocks: array of blocks to be written to storage
- Parameter completion: a closure that will be called after storing the blocks
*/ */
func write(blocks: [ZcashCompactBlock], completion: ((Error?) -> Void)?) func writeAsync(blocks: [ZcashCompactBlock]) async throws
/** /**
Remove every block above and including the given height. Remove every block above and including the given height.
@ -66,10 +63,7 @@ protocol CompactBlockRepository {
After this operation, the data store will look the same as one that has not yet stored the given block height. After this operation, the data store will look the same as one that has not yet stored the given block height.
Meaning, if max height is 100 block and rewindTo(50) is called, then the highest block remaining will be 49. Meaning, if max height is 100 block and rewindTo(50) is called, then the highest block remaining will be 49.
- Parameters:
- Parameter height: the height to rewind to - Parameter height: the height to rewind to
- Parameter completion: a closure that will be called after storing the blocks
*/ */
func rewind(to height: BlockHeight, completion: ((Error?) -> Void)?) func rewindAsync(to height: BlockHeight) async throws
} }

View File

@ -90,7 +90,8 @@ public extension BlockProgress {
public class LightWalletGRPCService { public class LightWalletGRPCService {
let channel: Channel let channel: Channel
let connectionManager: ConnectionStatusManager let connectionManager: ConnectionStatusManager
let compactTxStreamer: CompactTxStreamerClient let compactTxStreamer: CompactTxStreamerNIOClient
let compactTxStreamerAsync: CompactTxStreamerAsyncClient
let singleCallTimeout: TimeLimit let singleCallTimeout: TimeLimit
let streamingCallTimeout: TimeLimit let streamingCallTimeout: TimeLimit
@ -135,7 +136,14 @@ public class LightWalletGRPCService {
self.channel = channel self.channel = channel
compactTxStreamer = CompactTxStreamerClient( compactTxStreamer = CompactTxStreamerNIOClient(
channel: self.channel,
defaultCallOptions: Self.callOptions(
timeLimit: self.singleCallTimeout
)
)
compactTxStreamerAsync = CompactTxStreamerAsyncClient(
channel: self.channel, channel: self.channel,
defaultCallOptions: Self.callOptions( defaultCallOptions: Self.callOptions(
timeLimit: self.singleCallTimeout timeLimit: self.singleCallTimeout
@ -146,6 +154,7 @@ public class LightWalletGRPCService {
deinit { deinit {
_ = channel.close() _ = channel.close()
_ = compactTxStreamer.channel.close() _ = compactTxStreamer.channel.close()
_ = compactTxStreamerAsync.channel.close()
} }
func stop() { func stop() {
@ -155,7 +164,7 @@ public class LightWalletGRPCService {
func blockRange(startHeight: BlockHeight, endHeight: BlockHeight? = nil, result: @escaping (CompactBlock) -> Void) throws -> ServerStreamingCall<BlockRange, CompactBlock> { func blockRange(startHeight: BlockHeight, endHeight: BlockHeight? = nil, result: @escaping (CompactBlock) -> Void) throws -> ServerStreamingCall<BlockRange, CompactBlock> {
compactTxStreamer.getBlockRange(BlockRange(startHeight: startHeight, endHeight: endHeight), handler: result) compactTxStreamer.getBlockRange(BlockRange(startHeight: startHeight, endHeight: endHeight), handler: result)
} }
func latestBlock() throws -> BlockID { func latestBlock() throws -> BlockID {
try compactTxStreamer.getLatestBlock(ChainSpec()).response.wait() try compactTxStreamer.getLatestBlock(ChainSpec()).response.wait()
} }
@ -179,122 +188,18 @@ public class LightWalletGRPCService {
} }
} }
extension LightWalletGRPCService: LightWalletService { // MARK: - LightWalletServiceBlockingAPI
@discardableResult
public func blockStream( extension LightWalletGRPCService: LightWalletServiceBlockingAPI {
startHeight: BlockHeight,
endHeight: BlockHeight,
result: @escaping (Result<GRPCResult, LightWalletServiceError>) -> Void,
handler: @escaping (ZcashCompactBlock) -> Void,
progress: @escaping (BlockProgress) -> Void
) -> CancellableCall {
let future = compactTxStreamer.getBlockRange(
BlockRange(
startHeight: startHeight,
endHeight: endHeight
),
callOptions: Self.callOptions(timeLimit: self.streamingCallTimeout),
handler: { compactBlock in
handler(ZcashCompactBlock(compactBlock: compactBlock))
progress(
BlockProgress(
startHeight: startHeight,
targetHeight: endHeight,
progressHeight: BlockHeight(compactBlock.height)
)
)
}
)
future.status.whenComplete { completionResult in
switch completionResult {
case .success(let status):
switch status.code {
case .ok:
result(.success(GRPCResult.success))
default:
result(.failure(LightWalletServiceError.mapCode(status)))
}
case .failure(let error):
result(.failure(LightWalletServiceError.genericError(error: error)))
}
}
return future
}
public func getInfo() throws -> LightWalletdInfo { public func getInfo() throws -> LightWalletdInfo {
try compactTxStreamer.getLightdInfo(Empty()).response.wait() try compactTxStreamer.getLightdInfo(Empty()).response.wait()
} }
public func getInfo(result: @escaping (Result<LightWalletdInfo, LightWalletServiceError>) -> Void) { public func latestBlockHeight() throws -> BlockHeight {
compactTxStreamer.getLightdInfo(Empty()).response.whenComplete { completionResult in guard let height = try? compactTxStreamer.getLatestBlock(ChainSpec()).response.wait().compactBlockHeight() else {
switch completionResult { throw LightWalletServiceError.timeOut
case .success(let info):
result(.success(info))
case .failure(let error):
result(.failure(error.mapToServiceError()))
}
}
}
public func closeConnection() {
_ = channel.close()
}
public func fetchTransaction(txId: Data) throws -> TransactionEntity {
var txFilter = TxFilter()
txFilter.hash = txId
do {
let rawTx = try compactTxStreamer.getTransaction(txFilter).response.wait()
return TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)
} catch {
throw error.mapToServiceError()
}
}
public func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void) {
var txFilter = TxFilter()
txFilter.hash = txId
compactTxStreamer.getTransaction(txFilter).response.whenComplete { response in
switch response {
case .failure(let error):
result(.failure(error.mapToServiceError()))
case .success(let rawTx):
result(.success(TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)))
}
}
}
public func submit(spendTransaction: Data, result: @escaping (Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) {
do {
let transaction = try RawTransaction(serializedData: spendTransaction)
let response = self.compactTxStreamer.sendTransaction(transaction).response
response.whenComplete { responseResult in
switch responseResult {
case .failure(let error):
result(.failure(LightWalletServiceError.sentFailed(error: error)))
case .success(let success):
result(.success(success))
}
}
} catch {
result(.failure(error.mapToServiceError()))
}
}
public func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
let rawTx = RawTransaction.with { raw in
raw.data = spendTransaction
}
do {
return try compactTxStreamer.sendTransaction(rawTx).response.wait()
} catch {
throw error.mapToServiceError()
} }
return height
} }
public func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] { public func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] {
@ -315,51 +220,30 @@ extension LightWalletGRPCService: LightWalletService {
} }
} }
public func latestBlockHeight(result: @escaping (Result<BlockHeight, LightWalletServiceError>) -> Void) { public func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
let response = compactTxStreamer.getLatestBlock(ChainSpec()).response let rawTx = RawTransaction.with { raw in
raw.data = spendTransaction
response.whenSuccessBlocking(onto: queue) { blockID in
guard let blockHeight = Int(exactly: blockID.height) else {
result(.failure(LightWalletServiceError.generalError(message: "error creating blockheight from BlockID \(blockID)")))
return
}
result(.success(blockHeight))
} }
do {
response.whenFailureBlocking(onto: queue) { error in return try compactTxStreamer.sendTransaction(rawTx).response.wait()
result(.failure(error.mapToServiceError())) } catch {
throw error.mapToServiceError()
} }
} }
public func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void) { public func fetchTransaction(txId: Data) throws -> TransactionEntity {
queue.async { [weak self] in var txFilter = TxFilter()
guard let self = self else { return } txFilter.hash = txId
var blocks: [CompactBlock] = [] do {
let response = self.compactTxStreamer.getBlockRange(range.blockRange(), handler: { blocks.append($0) }) let rawTx = try compactTxStreamer.getTransaction(txFilter).response.wait()
do { return TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)
let status = try response.status.wait() } catch {
switch status.code { throw error.mapToServiceError()
case .ok:
result(.success(blocks.asZcashCompactBlocks()))
default:
result(.failure(.mapCode(status)))
}
} catch {
result(.failure(error.mapToServiceError()))
}
} }
} }
public func latestBlockHeight() throws -> BlockHeight {
guard let height = try? latestBlock().compactBlockHeight() else {
throw LightWalletServiceError.timeOut
}
return height
}
public func fetchUTXOs(for tAddress: String, height: BlockHeight) throws -> [UnspentTransactionOutputEntity] { public func fetchUTXOs(for tAddress: String, height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
let arg = GetAddressUtxosArg.with { utxoArgs in let arg = GetAddressUtxosArg.with { utxoArgs in
utxoArgs.addresses = [tAddress] utxoArgs.addresses = [tAddress]
@ -383,7 +267,168 @@ extension LightWalletGRPCService: LightWalletService {
} }
} }
public func fetchUTXOs(for tAddress: String, height: BlockHeight, result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void) { public func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
guard !tAddresses.isEmpty else {
return [] // FIXME: throw a real error
}
var utxos: [UnspentTransactionOutputEntity] = []
let arg = GetAddressUtxosArg.with { utxoArgs in
utxoArgs.addresses = tAddresses
utxoArgs.startHeight = UInt64(height)
}
utxos.append(
contentsOf:
try self.compactTxStreamer.getAddressUtxos(arg).response.wait().addressUtxos.map { reply in
UTXO(
id: nil,
address: reply.address,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
)
}
)
return utxos
}
}
// MARK: - LightWalletServiceNonBlockingAPI
extension LightWalletGRPCService: LightWalletServiceNonBlockingAPI {
public func getInfo(result: @escaping (Result<LightWalletdInfo, LightWalletServiceError>) -> Void) {
compactTxStreamer.getLightdInfo(Empty()).response.whenComplete { completionResult in
switch completionResult {
case .success(let info):
result(.success(info))
case .failure(let error):
result(.failure(error.mapToServiceError()))
}
}
}
public func getInfoAsync() async throws -> LightWalletdInfo {
try await compactTxStreamerAsync.getLightdInfo(Empty())
}
public func latestBlockHeight(result: @escaping (Result<BlockHeight, LightWalletServiceError>) -> Void) {
let response = compactTxStreamer.getLatestBlock(ChainSpec()).response
response.whenSuccessBlocking(onto: queue) { blockID in
guard let blockHeight = Int(exactly: blockID.height) else {
result(.failure(LightWalletServiceError.generalError(message: "error creating blockheight from BlockID \(blockID)")))
return
}
result(.success(blockHeight))
}
response.whenFailureBlocking(onto: queue) { error in
result(.failure(error.mapToServiceError()))
}
}
public func latestBlockHeightAsync() async throws -> BlockHeight {
try await BlockHeight(compactTxStreamerAsync.getLatestBlock(ChainSpec()).height)
}
public func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void) {
queue.async { [weak self] in
guard let self = self else { return }
var blocks: [CompactBlock] = []
let response = self.compactTxStreamer.getBlockRange(range.blockRange(), handler: { blocks.append($0) })
do {
let status = try response.status.wait()
switch status.code {
case .ok:
result(.success(blocks.asZcashCompactBlocks()))
default:
result(.failure(.mapCode(status)))
}
} catch {
result(.failure(error.mapToServiceError()))
}
}
}
public func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
let stream = compactTxStreamerAsync.getBlockRange(range.blockRange())
return AsyncThrowingStream { continuation in
Task {
do {
for try await block in stream {
continuation.yield(ZcashCompactBlock(compactBlock: block))
}
continuation.finish(throwing: nil)
} catch {
continuation.finish(throwing: error.mapToServiceError())
}
}
}
}
public func submit(spendTransaction: Data, result: @escaping (Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) {
do {
let transaction = try RawTransaction(serializedData: spendTransaction)
let response = self.compactTxStreamer.sendTransaction(transaction).response
response.whenComplete { responseResult in
switch responseResult {
case .failure(let error):
result(.failure(LightWalletServiceError.sentFailed(error: error)))
case .success(let success):
result(.success(success))
}
}
} catch {
result(.failure(error.mapToServiceError()))
}
}
public func submitAsync(spendTransaction: Data) async throws -> LightWalletServiceResponse {
do {
let transaction = try RawTransaction(serializedData: spendTransaction)
return try await compactTxStreamerAsync.sendTransaction(transaction)
} catch {
throw LightWalletServiceError.sentFailed(error: error)
}
}
public func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void) {
var txFilter = TxFilter()
txFilter.hash = txId
compactTxStreamer.getTransaction(txFilter).response.whenComplete { response in
switch response {
case .failure(let error):
result(.failure(error.mapToServiceError()))
case .success(let rawTx):
result(.success(TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)))
}
}
}
public func fetchTransactionAsync(txId: Data) async throws -> TransactionEntity {
var txFilter = TxFilter()
txFilter.hash = txId
let rawTx = try await compactTxStreamerAsync.getTransaction(txFilter)
return TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)
}
public func fetchUTXOs(
for tAddress: String,
height: BlockHeight,
result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>
) -> Void) {
queue.async { [weak self] in queue.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let arg = GetAddressUtxosArg.with { utxoArgs in let arg = GetAddressUtxosArg.with { utxoArgs in
@ -420,36 +465,13 @@ extension LightWalletGRPCService: LightWalletService {
} }
} }
public func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] { public func fetchUTXOs(
guard !tAddresses.isEmpty else { for tAddress: String,
return [] // FIXME: throw a real error height: BlockHeight
} ) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
return fetchUTXOs(for: [tAddress], height: height)
var utxos: [UnspentTransactionOutputEntity] = []
let arg = GetAddressUtxosArg.with { utxoArgs in
utxoArgs.addresses = tAddresses
utxoArgs.startHeight = UInt64(height)
}
utxos.append(
contentsOf:
try self.compactTxStreamer.getAddressUtxos(arg).response.wait().addressUtxos.map { reply in
UTXO(
id: nil,
address: reply.address,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
)
}
)
return utxos
} }
public func fetchUTXOs( public func fetchUTXOs(
for tAddresses: [String], for tAddresses: [String],
height: BlockHeight, height: BlockHeight,
@ -495,6 +517,119 @@ extension LightWalletGRPCService: LightWalletService {
} }
} }
} }
public func fetchUTXOs(
for tAddresses: [String],
height: BlockHeight
) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
guard !tAddresses.isEmpty else {
return AsyncThrowingStream { _ in }
}
let args = GetAddressUtxosArg.with { utxoArgs in
utxoArgs.addresses = tAddresses
utxoArgs.startHeight = UInt64(height)
}
let stream = compactTxStreamerAsync.getAddressUtxosStream(args)
return AsyncThrowingStream { continuation in
Task {
do {
for try await reply in stream {
continuation.yield(
UTXO(
id: nil,
address: reply.address,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
)
)
}
continuation.finish(throwing: nil)
} catch {
continuation.finish(throwing: error.mapToServiceError())
}
}
}
}
@discardableResult
public func blockStream(
startHeight: BlockHeight,
endHeight: BlockHeight,
result: @escaping (Result<GRPCResult, LightWalletServiceError>) -> Void,
handler: @escaping (ZcashCompactBlock) -> Void,
progress: @escaping (BlockProgress) -> Void
) -> CancellableCall {
let future = compactTxStreamer.getBlockRange(
BlockRange(
startHeight: startHeight,
endHeight: endHeight
),
callOptions: Self.callOptions(timeLimit: self.streamingCallTimeout),
handler: { compactBlock in
handler(ZcashCompactBlock(compactBlock: compactBlock))
progress(
BlockProgress(
startHeight: startHeight,
targetHeight: endHeight,
progressHeight: BlockHeight(compactBlock.height)
)
)
}
)
future.status.whenComplete { completionResult in
switch completionResult {
case .success(let status):
switch status.code {
case .ok:
result(.success(GRPCResult.success))
default:
result(.failure(LightWalletServiceError.mapCode(status)))
}
case .failure(let error):
result(.failure(LightWalletServiceError.genericError(error: error)))
}
}
return future
}
public func blockStream(
startHeight: BlockHeight,
endHeight: BlockHeight
) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
let stream = compactTxStreamerAsync.getBlockRange(
BlockRange(
startHeight: startHeight,
endHeight: endHeight
),
callOptions: Self.callOptions(timeLimit: self.streamingCallTimeout)
)
return AsyncThrowingStream { continuation in
Task {
do {
for try await compactBlock in stream {
continuation.yield(ZcashCompactBlock(compactBlock: compactBlock))
}
continuation.finish(throwing: nil)
} catch {
continuation.finish(throwing: error.mapToServiceError())
}
}
}
}
}
extension LightWalletGRPCService: LightWalletService {
public func closeConnection() {
_ = channel.close()
}
} }
// MARK: - Extensions // MARK: - Extensions

View File

@ -101,49 +101,102 @@ public protocol LightWalletServiceResponse {
extension SendResponse: LightWalletServiceResponse {} extension SendResponse: LightWalletServiceResponse {}
public protocol LightWalletService { /// Blocking API - used for the testing purposes
/** public protocol LightWalletServiceBlockingAPI {
returns the info for this lightwalletd server (blocking) /// Returns the info for this lightwalletd server (blocking)
*/
func getInfo() throws -> LightWalletdInfo func getInfo() throws -> LightWalletdInfo
/**
returns the info for this lightwalletd server
*/
func getInfo(result: @escaping (Result<LightWalletdInfo, LightWalletServiceError>) -> Void)
/** ///
Return the latest block height known to the service. /// Return the latest block height known to the service.
/// - Parameter result: a result containing the height or an Error
- Parameter result: a result containing the height or an Error
*/
func latestBlockHeight(result: @escaping (Result<BlockHeight, LightWalletServiceError>) -> Void)
/**
Return the latest block height known to the service.
- Parameter result: a result containing the height or an Error
*/
func latestBlockHeight() throws -> BlockHeight func latestBlockHeight() throws -> BlockHeight
/** /// Return the given range of blocks.
Return the given range of blocks. ///
/// - Parameter range: the inclusive range to fetch.
- Parameter range: the inclusive range to fetch. /// For instance if 1..5 is given, then every block in that will be fetched, including 1 and 5.
For instance if 1..5 is given, then every block in that will be fetched, including 1 and 5.
Non blocking
*/
func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void )
/**
Return the given range of blocks.
- Parameter range: the inclusive range to fetch.
For instance if 1..5 is given, then every block in that will be fetched, including 1 and 5.
blocking
*/
func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock]
/// Submits a raw transaction over lightwalletd. Blocking
/// - Parameter spendTransaction: data representing the transaction to be sent
/// - Throws: LightWalletServiceError
/// - Returns: LightWalletServiceResponse
func submit(spendTransaction: Data) throws -> LightWalletServiceResponse
/// Gets a transaction by id
/// - Parameter txId: data representing the transaction ID
/// - Throws: LightWalletServiceError
/// - Returns: LightWalletServiceResponse
func fetchTransaction(txId: Data) throws -> TransactionEntity
func fetchUTXOs(
for tAddress: String,
height: BlockHeight
) throws -> [UnspentTransactionOutputEntity]
func fetchUTXOs(
for tAddresses: [String],
height: BlockHeight
) throws -> [UnspentTransactionOutputEntity]
}
public protocol LightWalletServiceNonBlockingAPI {
/// Returns the info for this lightwalletd server
@available(*, deprecated, message: "This function will be removed soon. Use the `getInfoAsync()` instead.")
func getInfo(result: @escaping (Result<LightWalletdInfo, LightWalletServiceError>) -> Void)
func getInfoAsync() async throws -> LightWalletdInfo
///
/// Return the latest block height known to the service.
/// - Parameter result: a result containing the height or an Error
@available(*, deprecated, message: "This function will be removed soon. Use the `latestBlockHeightAsync()` instead.")
func latestBlockHeight(result: @escaping (Result<BlockHeight, LightWalletServiceError>) -> Void)
func latestBlockHeightAsync() async throws -> BlockHeight
/// Return the given range of blocks.
/// - Parameter range: the inclusive range to fetch.
/// For instance if 1..5 is given, then every block in that will be fetched, including 1 and 5.
@available(*, deprecated, message: "This function will be removed soon. Use the `blockRange(...) -> AsyncThrowingStream<ZcashCompactBlock, Error>` instead.")
func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void)
func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error>
/// Submits a raw transaction over lightwalletd. Non-Blocking
/// - Parameter spendTransaction: data representing the transaction to be sent
/// - Parameter result: escaping closure that takes a result containing either LightWalletServiceResponse or LightWalletServiceError
@available(*, deprecated, message: "This function will be removed soon. Use the `submitAsync(spendTransaction: Data)` instead.")
func submit(spendTransaction: Data, result: @escaping(Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void)
func submitAsync(spendTransaction: Data) async throws -> LightWalletServiceResponse
/// Gets a transaction by id
/// - Parameter txId: data representing the transaction ID
/// - Parameter result: handler for the result
/// - Throws: LightWalletServiceError
/// - Returns: LightWalletServiceResponse
@available(*, deprecated, message: "This function will be removed soon. Use the `fetchTransactionAsync(txId: Data)` instead.")
func fetchTransaction(
txId: Data,
result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void
)
func fetchTransactionAsync(txId: Data) async throws -> TransactionEntity
@available(*, deprecated, message: "This function will be removed soon. Use the `fetchUTXOs(for tAddress:...) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>` instead.")
func fetchUTXOs(
for tAddress: String,
height: BlockHeight,
result: @escaping(Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
)
func fetchUTXOs(for tAddress: String, height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
@available(*, deprecated, message: "This function will be removed soon. Use the `fetchUTXOs(for tAddresses:...) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>` instead.")
func fetchUTXOs(
for tAddresses: [String],
height: BlockHeight,
result: @escaping(Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
)
func fetchUTXOs(for tAddresses: [String], height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
@available(*, deprecated, message: "This function will be removed soon. Use the `blockStream(...) -> AsyncThrowingStream<ZcashCompactBlock, Error>` instead.")
@discardableResult @discardableResult
func blockStream( func blockStream(
startHeight: BlockHeight, startHeight: BlockHeight,
@ -153,62 +206,12 @@ public protocol LightWalletService {
progress: @escaping (BlockProgress) -> Void progress: @escaping (BlockProgress) -> Void
) -> CancellableCall ) -> CancellableCall
/** func blockStream(
Submits a raw transaction over lightwalletd. Non-Blocking startHeight: BlockHeight,
- Parameter spendTransaction: data representing the transaction to be sent endHeight: BlockHeight
- Parameter result: escaping closure that takes a result containing either LightWalletServiceResponse or LightWalletServiceError ) -> AsyncThrowingStream<ZcashCompactBlock, Error>
*/ }
func submit(spendTransaction: Data, result: @escaping(Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void)
public protocol LightWalletService: LightWalletServiceNonBlockingAPI, LightWalletServiceBlockingAPI {
/**
Submits a raw transaction over lightwalletd. Blocking
- Parameter spendTransaction: data representing the transaction to be sent
- Throws: LightWalletServiceError
- Returns: LightWalletServiceResponse
*/
func submit(spendTransaction: Data) throws -> LightWalletServiceResponse
/**
Gets a transaction by id
- Parameter txId: data representing the transaction ID
- Throws: LightWalletServiceError
- Returns: LightWalletServiceResponse
*/
func fetchTransaction(txId: Data) throws -> TransactionEntity
/**
Gets a transaction by id
- Parameter txId: data representing the transaction ID
- Parameter result: handler for the result
- Throws: LightWalletServiceError
- Returns: LightWalletServiceResponse
*/
func fetchTransaction(
txId: Data,
result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void
)
func fetchUTXOs(
for tAddress: String,
height: BlockHeight
) throws -> [UnspentTransactionOutputEntity]
func fetchUTXOs(
for tAddress: String,
height: BlockHeight,
result: @escaping(Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
)
func fetchUTXOs(
for tAddresses: [String],
height: BlockHeight
) throws -> [UnspentTransactionOutputEntity]
func fetchUTXOs(
for tAddresses: [String],
height: BlockHeight,
result: @escaping(Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
)
func closeConnection() func closeConnection()
} }

View File

@ -22,11 +22,15 @@
// //
import GRPC import GRPC
import NIO import NIO
import NIOConcurrencyHelpers
import SwiftProtobuf import SwiftProtobuf
/// Usage: instantiate CompactTxStreamerClient, then call methods of this protocol to make API calls. /// Usage: instantiate `CompactTxStreamerClient`, then call methods of this protocol to make API calls.
internal protocol CompactTxStreamerClientProtocol: GRPCClient { internal protocol CompactTxStreamerClientProtocol: GRPCClient {
var serviceName: String { get }
var interceptors: CompactTxStreamerClientInterceptorFactoryProtocol? { get }
func getLatestBlock( func getLatestBlock(
_ request: ChainSpec, _ request: ChainSpec,
callOptions: CallOptions? callOptions: CallOptions?
@ -99,10 +103,12 @@ internal protocol CompactTxStreamerClientProtocol: GRPCClient {
_ request: Duration, _ request: Duration,
callOptions: CallOptions? callOptions: CallOptions?
) -> UnaryCall<Duration, PingResponse> ) -> UnaryCall<Duration, PingResponse>
} }
extension CompactTxStreamerClientProtocol { extension CompactTxStreamerClientProtocol {
internal var serviceName: String {
return "cash.z.wallet.sdk.rpc.CompactTxStreamer"
}
/// Return the height of the tip of the best chain /// Return the height of the tip of the best chain
/// ///
@ -115,9 +121,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<ChainSpec, BlockID> { ) -> UnaryCall<ChainSpec, BlockID> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock", path: CompactTxStreamerClientMetadata.Methods.getLatestBlock.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetLatestBlockInterceptors() ?? []
) )
} }
@ -132,9 +139,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<BlockID, CompactBlock> { ) -> UnaryCall<BlockID, CompactBlock> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock", path: CompactTxStreamerClientMetadata.Methods.getBlock.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetBlockInterceptors() ?? []
) )
} }
@ -151,9 +159,10 @@ extension CompactTxStreamerClientProtocol {
handler: @escaping (CompactBlock) -> Void handler: @escaping (CompactBlock) -> Void
) -> ServerStreamingCall<BlockRange, CompactBlock> { ) -> ServerStreamingCall<BlockRange, CompactBlock> {
return self.makeServerStreamingCall( return self.makeServerStreamingCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange", path: CompactTxStreamerClientMetadata.Methods.getBlockRange.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions, callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetBlockRangeInterceptors() ?? [],
handler: handler handler: handler
) )
} }
@ -169,9 +178,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<TxFilter, RawTransaction> { ) -> UnaryCall<TxFilter, RawTransaction> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction", path: CompactTxStreamerClientMetadata.Methods.getTransaction.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTransactionInterceptors() ?? []
) )
} }
@ -186,9 +196,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<RawTransaction, SendResponse> { ) -> UnaryCall<RawTransaction, SendResponse> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction", path: CompactTxStreamerClientMetadata.Methods.sendTransaction.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeSendTransactionInterceptors() ?? []
) )
} }
@ -205,9 +216,10 @@ extension CompactTxStreamerClientProtocol {
handler: @escaping (RawTransaction) -> Void handler: @escaping (RawTransaction) -> Void
) -> ServerStreamingCall<TransparentAddressBlockFilter, RawTransaction> { ) -> ServerStreamingCall<TransparentAddressBlockFilter, RawTransaction> {
return self.makeServerStreamingCall( return self.makeServerStreamingCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTxids", path: CompactTxStreamerClientMetadata.Methods.getTaddressTxids.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions, callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressTxidsInterceptors() ?? [],
handler: handler handler: handler
) )
} }
@ -223,9 +235,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<AddressList, Balance> { ) -> UnaryCall<AddressList, Balance> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalance", path: CompactTxStreamerClientMetadata.Methods.getTaddressBalance.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressBalanceInterceptors() ?? []
) )
} }
@ -241,8 +254,9 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> ClientStreamingCall<Address, Balance> { ) -> ClientStreamingCall<Address, Balance> {
return self.makeClientStreamingCall( return self.makeClientStreamingCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalanceStream", path: CompactTxStreamerClientMetadata.Methods.getTaddressBalanceStream.path,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressBalanceStreamInterceptors() ?? []
) )
} }
@ -267,9 +281,10 @@ extension CompactTxStreamerClientProtocol {
handler: @escaping (CompactTx) -> Void handler: @escaping (CompactTx) -> Void
) -> ServerStreamingCall<Exclude, CompactTx> { ) -> ServerStreamingCall<Exclude, CompactTx> {
return self.makeServerStreamingCall( return self.makeServerStreamingCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolTx", path: CompactTxStreamerClientMetadata.Methods.getMempoolTx.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions, callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetMempoolTxInterceptors() ?? [],
handler: handler handler: handler
) )
} }
@ -288,9 +303,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<BlockID, TreeState> { ) -> UnaryCall<BlockID, TreeState> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTreeState", path: CompactTxStreamerClientMetadata.Methods.getTreeState.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTreeStateInterceptors() ?? []
) )
} }
@ -305,9 +321,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<GetAddressUtxosArg, GetAddressUtxosReplyList> { ) -> UnaryCall<GetAddressUtxosArg, GetAddressUtxosReplyList> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxos", path: CompactTxStreamerClientMetadata.Methods.getAddressUtxos.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetAddressUtxosInterceptors() ?? []
) )
} }
@ -324,9 +341,10 @@ extension CompactTxStreamerClientProtocol {
handler: @escaping (GetAddressUtxosReply) -> Void handler: @escaping (GetAddressUtxosReply) -> Void
) -> ServerStreamingCall<GetAddressUtxosArg, GetAddressUtxosReply> { ) -> ServerStreamingCall<GetAddressUtxosArg, GetAddressUtxosReply> {
return self.makeServerStreamingCall( return self.makeServerStreamingCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxosStream", path: CompactTxStreamerClientMetadata.Methods.getAddressUtxosStream.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions, callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetAddressUtxosStreamInterceptors() ?? [],
handler: handler handler: handler
) )
} }
@ -342,9 +360,10 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<Empty, LightdInfo> { ) -> UnaryCall<Empty, LightdInfo> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo", path: CompactTxStreamerClientMetadata.Methods.getLightdInfo.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetLightdInfoInterceptors() ?? []
) )
} }
@ -359,25 +378,679 @@ extension CompactTxStreamerClientProtocol {
callOptions: CallOptions? = nil callOptions: CallOptions? = nil
) -> UnaryCall<Duration, PingResponse> { ) -> UnaryCall<Duration, PingResponse> {
return self.makeUnaryCall( return self.makeUnaryCall(
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/Ping", path: CompactTxStreamerClientMetadata.Methods.ping.path,
request: request, request: request,
callOptions: callOptions ?? self.defaultCallOptions callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makePingInterceptors() ?? []
) )
} }
} }
#if compiler(>=5.6)
@available(*, deprecated)
extension CompactTxStreamerClient: @unchecked Sendable {}
#endif // compiler(>=5.6)
@available(*, deprecated, renamed: "CompactTxStreamerNIOClient")
internal final class CompactTxStreamerClient: CompactTxStreamerClientProtocol { internal final class CompactTxStreamerClient: CompactTxStreamerClientProtocol {
private let lock = Lock()
private var _defaultCallOptions: CallOptions
private var _interceptors: CompactTxStreamerClientInterceptorFactoryProtocol?
internal let channel: GRPCChannel internal let channel: GRPCChannel
internal var defaultCallOptions: CallOptions internal var defaultCallOptions: CallOptions {
get { self.lock.withLock { return self._defaultCallOptions } }
set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }
}
internal var interceptors: CompactTxStreamerClientInterceptorFactoryProtocol? {
get { self.lock.withLock { return self._interceptors } }
set { self.lock.withLockVoid { self._interceptors = newValue } }
}
/// Creates a client for the cash.z.wallet.sdk.rpc.CompactTxStreamer service. /// Creates a client for the cash.z.wallet.sdk.rpc.CompactTxStreamer service.
/// ///
/// - Parameters: /// - Parameters:
/// - channel: `GRPCChannel` to the service host. /// - channel: `GRPCChannel` to the service host.
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
internal init(channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions()) { /// - interceptors: A factory providing interceptors for each RPC.
internal init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: CompactTxStreamerClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel self.channel = channel
self.defaultCallOptions = defaultCallOptions self._defaultCallOptions = defaultCallOptions
self._interceptors = interceptors
}
}
internal struct CompactTxStreamerNIOClient: CompactTxStreamerClientProtocol {
internal var channel: GRPCChannel
internal var defaultCallOptions: CallOptions
internal var interceptors: CompactTxStreamerClientInterceptorFactoryProtocol?
/// Creates a client for the cash.z.wallet.sdk.rpc.CompactTxStreamer service.
///
/// - Parameters:
/// - channel: `GRPCChannel` to the service host.
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
/// - interceptors: A factory providing interceptors for each RPC.
internal init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: CompactTxStreamerClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self.defaultCallOptions = defaultCallOptions
self.interceptors = interceptors
}
}
#if compiler(>=5.6)
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
internal protocol CompactTxStreamerAsyncClientProtocol: GRPCClient {
static var serviceDescriptor: GRPCServiceDescriptor { get }
var interceptors: CompactTxStreamerClientInterceptorFactoryProtocol? { get }
func makeGetLatestBlockCall(
_ request: ChainSpec,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<ChainSpec, BlockID>
func makeGetBlockCall(
_ request: BlockID,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<BlockID, CompactBlock>
func makeGetBlockRangeCall(
_ request: BlockRange,
callOptions: CallOptions?
) -> GRPCAsyncServerStreamingCall<BlockRange, CompactBlock>
func makeGetTransactionCall(
_ request: TxFilter,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<TxFilter, RawTransaction>
func makeSendTransactionCall(
_ request: RawTransaction,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<RawTransaction, SendResponse>
func makeGetTaddressTxidsCall(
_ request: TransparentAddressBlockFilter,
callOptions: CallOptions?
) -> GRPCAsyncServerStreamingCall<TransparentAddressBlockFilter, RawTransaction>
func makeGetTaddressBalanceCall(
_ request: AddressList,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<AddressList, Balance>
func makeGetTaddressBalanceStreamCall(
callOptions: CallOptions?
) -> GRPCAsyncClientStreamingCall<Address, Balance>
func makeGetMempoolTxCall(
_ request: Exclude,
callOptions: CallOptions?
) -> GRPCAsyncServerStreamingCall<Exclude, CompactTx>
func makeGetTreeStateCall(
_ request: BlockID,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<BlockID, TreeState>
func makeGetAddressUtxosCall(
_ request: GetAddressUtxosArg,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<GetAddressUtxosArg, GetAddressUtxosReplyList>
func makeGetAddressUtxosStreamCall(
_ request: GetAddressUtxosArg,
callOptions: CallOptions?
) -> GRPCAsyncServerStreamingCall<GetAddressUtxosArg, GetAddressUtxosReply>
func makeGetLightdInfoCall(
_ request: Empty,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Empty, LightdInfo>
func makePingCall(
_ request: Duration,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Duration, PingResponse>
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CompactTxStreamerAsyncClientProtocol {
internal static var serviceDescriptor: GRPCServiceDescriptor {
return CompactTxStreamerClientMetadata.serviceDescriptor
}
internal var interceptors: CompactTxStreamerClientInterceptorFactoryProtocol? {
return nil
}
internal func makeGetLatestBlockCall(
_ request: ChainSpec,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<ChainSpec, BlockID> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getLatestBlock.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetLatestBlockInterceptors() ?? []
)
}
internal func makeGetBlockCall(
_ request: BlockID,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<BlockID, CompactBlock> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getBlock.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetBlockInterceptors() ?? []
)
}
internal func makeGetBlockRangeCall(
_ request: BlockRange,
callOptions: CallOptions? = nil
) -> GRPCAsyncServerStreamingCall<BlockRange, CompactBlock> {
return self.makeAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getBlockRange.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetBlockRangeInterceptors() ?? []
)
}
internal func makeGetTransactionCall(
_ request: TxFilter,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<TxFilter, RawTransaction> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getTransaction.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTransactionInterceptors() ?? []
)
}
internal func makeSendTransactionCall(
_ request: RawTransaction,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<RawTransaction, SendResponse> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.sendTransaction.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeSendTransactionInterceptors() ?? []
)
}
internal func makeGetTaddressTxidsCall(
_ request: TransparentAddressBlockFilter,
callOptions: CallOptions? = nil
) -> GRPCAsyncServerStreamingCall<TransparentAddressBlockFilter, RawTransaction> {
return self.makeAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getTaddressTxids.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressTxidsInterceptors() ?? []
)
}
internal func makeGetTaddressBalanceCall(
_ request: AddressList,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<AddressList, Balance> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getTaddressBalance.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressBalanceInterceptors() ?? []
)
}
internal func makeGetTaddressBalanceStreamCall(
callOptions: CallOptions? = nil
) -> GRPCAsyncClientStreamingCall<Address, Balance> {
return self.makeAsyncClientStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getTaddressBalanceStream.path,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressBalanceStreamInterceptors() ?? []
)
}
internal func makeGetMempoolTxCall(
_ request: Exclude,
callOptions: CallOptions? = nil
) -> GRPCAsyncServerStreamingCall<Exclude, CompactTx> {
return self.makeAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getMempoolTx.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetMempoolTxInterceptors() ?? []
)
}
internal func makeGetTreeStateCall(
_ request: BlockID,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<BlockID, TreeState> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getTreeState.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTreeStateInterceptors() ?? []
)
}
internal func makeGetAddressUtxosCall(
_ request: GetAddressUtxosArg,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<GetAddressUtxosArg, GetAddressUtxosReplyList> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getAddressUtxos.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetAddressUtxosInterceptors() ?? []
)
}
internal func makeGetAddressUtxosStreamCall(
_ request: GetAddressUtxosArg,
callOptions: CallOptions? = nil
) -> GRPCAsyncServerStreamingCall<GetAddressUtxosArg, GetAddressUtxosReply> {
return self.makeAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getAddressUtxosStream.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetAddressUtxosStreamInterceptors() ?? []
)
}
internal func makeGetLightdInfoCall(
_ request: Empty,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<Empty, LightdInfo> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getLightdInfo.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetLightdInfoInterceptors() ?? []
)
}
internal func makePingCall(
_ request: Duration,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<Duration, PingResponse> {
return self.makeAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.ping.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makePingInterceptors() ?? []
)
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CompactTxStreamerAsyncClientProtocol {
internal func getLatestBlock(
_ request: ChainSpec,
callOptions: CallOptions? = nil
) async throws -> BlockID {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getLatestBlock.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetLatestBlockInterceptors() ?? []
)
}
internal func getBlock(
_ request: BlockID,
callOptions: CallOptions? = nil
) async throws -> CompactBlock {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getBlock.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetBlockInterceptors() ?? []
)
}
internal func getBlockRange(
_ request: BlockRange,
callOptions: CallOptions? = nil
) -> GRPCAsyncResponseStream<CompactBlock> {
return self.performAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getBlockRange.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetBlockRangeInterceptors() ?? []
)
}
internal func getTransaction(
_ request: TxFilter,
callOptions: CallOptions? = nil
) async throws -> RawTransaction {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getTransaction.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTransactionInterceptors() ?? []
)
}
internal func sendTransaction(
_ request: RawTransaction,
callOptions: CallOptions? = nil
) async throws -> SendResponse {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.sendTransaction.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeSendTransactionInterceptors() ?? []
)
}
internal func getTaddressTxids(
_ request: TransparentAddressBlockFilter,
callOptions: CallOptions? = nil
) -> GRPCAsyncResponseStream<RawTransaction> {
return self.performAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getTaddressTxids.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressTxidsInterceptors() ?? []
)
}
internal func getTaddressBalance(
_ request: AddressList,
callOptions: CallOptions? = nil
) async throws -> Balance {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getTaddressBalance.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressBalanceInterceptors() ?? []
)
}
internal func getTaddressBalanceStream<RequestStream>(
_ requests: RequestStream,
callOptions: CallOptions? = nil
) async throws -> Balance where RequestStream: Sequence, RequestStream.Element == Address {
return try await self.performAsyncClientStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getTaddressBalanceStream.path,
requests: requests,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressBalanceStreamInterceptors() ?? []
)
}
internal func getTaddressBalanceStream<RequestStream>(
_ requests: RequestStream,
callOptions: CallOptions? = nil
) async throws -> Balance where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Address {
return try await self.performAsyncClientStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getTaddressBalanceStream.path,
requests: requests,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTaddressBalanceStreamInterceptors() ?? []
)
}
internal func getMempoolTx(
_ request: Exclude,
callOptions: CallOptions? = nil
) -> GRPCAsyncResponseStream<CompactTx> {
return self.performAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getMempoolTx.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetMempoolTxInterceptors() ?? []
)
}
internal func getTreeState(
_ request: BlockID,
callOptions: CallOptions? = nil
) async throws -> TreeState {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getTreeState.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetTreeStateInterceptors() ?? []
)
}
internal func getAddressUtxos(
_ request: GetAddressUtxosArg,
callOptions: CallOptions? = nil
) async throws -> GetAddressUtxosReplyList {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getAddressUtxos.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetAddressUtxosInterceptors() ?? []
)
}
internal func getAddressUtxosStream(
_ request: GetAddressUtxosArg,
callOptions: CallOptions? = nil
) -> GRPCAsyncResponseStream<GetAddressUtxosReply> {
return self.performAsyncServerStreamingCall(
path: CompactTxStreamerClientMetadata.Methods.getAddressUtxosStream.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetAddressUtxosStreamInterceptors() ?? []
)
}
internal func getLightdInfo(
_ request: Empty,
callOptions: CallOptions? = nil
) async throws -> LightdInfo {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.getLightdInfo.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeGetLightdInfoInterceptors() ?? []
)
}
internal func ping(
_ request: Duration,
callOptions: CallOptions? = nil
) async throws -> PingResponse {
return try await self.performAsyncUnaryCall(
path: CompactTxStreamerClientMetadata.Methods.ping.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makePingInterceptors() ?? []
)
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
internal struct CompactTxStreamerAsyncClient: CompactTxStreamerAsyncClientProtocol {
internal var channel: GRPCChannel
internal var defaultCallOptions: CallOptions
internal var interceptors: CompactTxStreamerClientInterceptorFactoryProtocol?
internal init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: CompactTxStreamerClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self.defaultCallOptions = defaultCallOptions
self.interceptors = interceptors
}
}
#endif // compiler(>=5.6)
internal protocol CompactTxStreamerClientInterceptorFactoryProtocol: GRPCSendable {
/// - Returns: Interceptors to use when invoking 'getLatestBlock'.
func makeGetLatestBlockInterceptors() -> [ClientInterceptor<ChainSpec, BlockID>]
/// - Returns: Interceptors to use when invoking 'getBlock'.
func makeGetBlockInterceptors() -> [ClientInterceptor<BlockID, CompactBlock>]
/// - Returns: Interceptors to use when invoking 'getBlockRange'.
func makeGetBlockRangeInterceptors() -> [ClientInterceptor<BlockRange, CompactBlock>]
/// - Returns: Interceptors to use when invoking 'getTransaction'.
func makeGetTransactionInterceptors() -> [ClientInterceptor<TxFilter, RawTransaction>]
/// - Returns: Interceptors to use when invoking 'sendTransaction'.
func makeSendTransactionInterceptors() -> [ClientInterceptor<RawTransaction, SendResponse>]
/// - Returns: Interceptors to use when invoking 'getTaddressTxids'.
func makeGetTaddressTxidsInterceptors() -> [ClientInterceptor<TransparentAddressBlockFilter, RawTransaction>]
/// - Returns: Interceptors to use when invoking 'getTaddressBalance'.
func makeGetTaddressBalanceInterceptors() -> [ClientInterceptor<AddressList, Balance>]
/// - Returns: Interceptors to use when invoking 'getTaddressBalanceStream'.
func makeGetTaddressBalanceStreamInterceptors() -> [ClientInterceptor<Address, Balance>]
/// - Returns: Interceptors to use when invoking 'getMempoolTx'.
func makeGetMempoolTxInterceptors() -> [ClientInterceptor<Exclude, CompactTx>]
/// - Returns: Interceptors to use when invoking 'getTreeState'.
func makeGetTreeStateInterceptors() -> [ClientInterceptor<BlockID, TreeState>]
/// - Returns: Interceptors to use when invoking 'getAddressUtxos'.
func makeGetAddressUtxosInterceptors() -> [ClientInterceptor<GetAddressUtxosArg, GetAddressUtxosReplyList>]
/// - Returns: Interceptors to use when invoking 'getAddressUtxosStream'.
func makeGetAddressUtxosStreamInterceptors() -> [ClientInterceptor<GetAddressUtxosArg, GetAddressUtxosReply>]
/// - Returns: Interceptors to use when invoking 'getLightdInfo'.
func makeGetLightdInfoInterceptors() -> [ClientInterceptor<Empty, LightdInfo>]
/// - Returns: Interceptors to use when invoking 'ping'.
func makePingInterceptors() -> [ClientInterceptor<Duration, PingResponse>]
}
internal enum CompactTxStreamerClientMetadata {
internal static let serviceDescriptor = GRPCServiceDescriptor(
name: "CompactTxStreamer",
fullName: "cash.z.wallet.sdk.rpc.CompactTxStreamer",
methods: [
CompactTxStreamerClientMetadata.Methods.getLatestBlock,
CompactTxStreamerClientMetadata.Methods.getBlock,
CompactTxStreamerClientMetadata.Methods.getBlockRange,
CompactTxStreamerClientMetadata.Methods.getTransaction,
CompactTxStreamerClientMetadata.Methods.sendTransaction,
CompactTxStreamerClientMetadata.Methods.getTaddressTxids,
CompactTxStreamerClientMetadata.Methods.getTaddressBalance,
CompactTxStreamerClientMetadata.Methods.getTaddressBalanceStream,
CompactTxStreamerClientMetadata.Methods.getMempoolTx,
CompactTxStreamerClientMetadata.Methods.getTreeState,
CompactTxStreamerClientMetadata.Methods.getAddressUtxos,
CompactTxStreamerClientMetadata.Methods.getAddressUtxosStream,
CompactTxStreamerClientMetadata.Methods.getLightdInfo,
CompactTxStreamerClientMetadata.Methods.ping,
]
)
internal enum Methods {
internal static let getLatestBlock = GRPCMethodDescriptor(
name: "GetLatestBlock",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock",
type: GRPCCallType.unary
)
internal static let getBlock = GRPCMethodDescriptor(
name: "GetBlock",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock",
type: GRPCCallType.unary
)
internal static let getBlockRange = GRPCMethodDescriptor(
name: "GetBlockRange",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange",
type: GRPCCallType.serverStreaming
)
internal static let getTransaction = GRPCMethodDescriptor(
name: "GetTransaction",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction",
type: GRPCCallType.unary
)
internal static let sendTransaction = GRPCMethodDescriptor(
name: "SendTransaction",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction",
type: GRPCCallType.unary
)
internal static let getTaddressTxids = GRPCMethodDescriptor(
name: "GetTaddressTxids",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTxids",
type: GRPCCallType.serverStreaming
)
internal static let getTaddressBalance = GRPCMethodDescriptor(
name: "GetTaddressBalance",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalance",
type: GRPCCallType.unary
)
internal static let getTaddressBalanceStream = GRPCMethodDescriptor(
name: "GetTaddressBalanceStream",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalanceStream",
type: GRPCCallType.clientStreaming
)
internal static let getMempoolTx = GRPCMethodDescriptor(
name: "GetMempoolTx",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolTx",
type: GRPCCallType.serverStreaming
)
internal static let getTreeState = GRPCMethodDescriptor(
name: "GetTreeState",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTreeState",
type: GRPCCallType.unary
)
internal static let getAddressUtxos = GRPCMethodDescriptor(
name: "GetAddressUtxos",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxos",
type: GRPCCallType.unary
)
internal static let getAddressUtxosStream = GRPCMethodDescriptor(
name: "GetAddressUtxosStream",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxosStream",
type: GRPCCallType.serverStreaming
)
internal static let getLightdInfo = GRPCMethodDescriptor(
name: "GetLightdInfo",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo",
type: GRPCCallType.unary
)
internal static let ping = GRPCMethodDescriptor(
name: "Ping",
path: "/cash.z.wallet.sdk.rpc.CompactTxStreamer/Ping",
type: GRPCCallType.unary
)
} }
} }

View File

@ -33,7 +33,7 @@ struct BlockID {
var height: UInt64 = 0 var height: UInt64 = 0
var hash: Data = SwiftProtobuf.Internal.emptyData var hash: Data = Data()
var unknownFields = SwiftProtobuf.UnknownStorage() var unknownFields = SwiftProtobuf.UnknownStorage()
@ -95,7 +95,7 @@ struct TxFilter {
var index: UInt64 = 0 var index: UInt64 = 0
/// transaction ID (hash, txid) /// transaction ID (hash, txid)
var hash: Data = SwiftProtobuf.Internal.emptyData var hash: Data = Data()
var unknownFields = SwiftProtobuf.UnknownStorage() var unknownFields = SwiftProtobuf.UnknownStorage()
@ -112,7 +112,7 @@ struct RawTransaction {
// methods supported on all messages. // methods supported on all messages.
/// exact data returned by Zcash 'getrawtransaction' /// exact data returned by Zcash 'getrawtransaction'
var data: Data = SwiftProtobuf.Internal.emptyData var data: Data = Data()
/// height that the transaction was mined (or -1) /// height that the transaction was mined (or -1)
var height: UInt64 = 0 var height: UInt64 = 0
@ -367,11 +367,11 @@ struct GetAddressUtxosReply {
var address: String = String() var address: String = String()
var txid: Data = SwiftProtobuf.Internal.emptyData var txid: Data = Data()
var index: Int32 = 0 var index: Int32 = 0
var script: Data = SwiftProtobuf.Internal.emptyData var script: Data = Data()
var valueZat: Int64 = 0 var valueZat: Int64 = 0
@ -394,6 +394,28 @@ struct GetAddressUtxosReplyList {
init() {} init() {}
} }
#if swift(>=5.5) && canImport(_Concurrency)
extension BlockID: @unchecked Sendable {}
extension BlockRange: @unchecked Sendable {}
extension TxFilter: @unchecked Sendable {}
extension RawTransaction: @unchecked Sendable {}
extension SendResponse: @unchecked Sendable {}
extension ChainSpec: @unchecked Sendable {}
extension Empty: @unchecked Sendable {}
extension LightdInfo: @unchecked Sendable {}
extension TransparentAddressBlockFilter: @unchecked Sendable {}
extension Duration: @unchecked Sendable {}
extension PingResponse: @unchecked Sendable {}
extension Address: @unchecked Sendable {}
extension AddressList: @unchecked Sendable {}
extension Balance: @unchecked Sendable {}
extension Exclude: @unchecked Sendable {}
extension TreeState: @unchecked Sendable {}
extension GetAddressUtxosArg: @unchecked Sendable {}
extension GetAddressUtxosReply: @unchecked Sendable {}
extension GetAddressUtxosReplyList: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime. // MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "cash.z.wallet.sdk.rpc" fileprivate let _protobuf_package = "cash.z.wallet.sdk.rpc"
@ -407,9 +429,12 @@ extension BlockID: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularUInt64Field(value: &self.height) case 1: try { try decoder.decodeSingularUInt64Field(value: &self.height) }()
case 2: try decoder.decodeSingularBytesField(value: &self.hash) case 2: try { try decoder.decodeSingularBytesField(value: &self.hash) }()
default: break default: break
} }
} }
@ -442,21 +467,28 @@ extension BlockRange: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularMessageField(value: &self._start) case 1: try { try decoder.decodeSingularMessageField(value: &self._start) }()
case 2: try decoder.decodeSingularMessageField(value: &self._end) case 2: try { try decoder.decodeSingularMessageField(value: &self._end) }()
default: break default: break
} }
} }
} }
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._start { // The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._start {
try visitor.visitSingularMessageField(value: v, fieldNumber: 1) try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
} } }()
if let v = self._end { try { if let v = self._end {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2) try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} } }()
try unknownFields.traverse(visitor: &visitor) try unknownFields.traverse(visitor: &visitor)
} }
@ -478,19 +510,26 @@ extension TxFilter: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularMessageField(value: &self._block) case 1: try { try decoder.decodeSingularMessageField(value: &self._block) }()
case 2: try decoder.decodeSingularUInt64Field(value: &self.index) case 2: try { try decoder.decodeSingularUInt64Field(value: &self.index) }()
case 3: try decoder.decodeSingularBytesField(value: &self.hash) case 3: try { try decoder.decodeSingularBytesField(value: &self.hash) }()
default: break default: break
} }
} }
} }
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._block { // The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._block {
try visitor.visitSingularMessageField(value: v, fieldNumber: 1) try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
} } }()
if self.index != 0 { if self.index != 0 {
try visitor.visitSingularUInt64Field(value: self.index, fieldNumber: 2) try visitor.visitSingularUInt64Field(value: self.index, fieldNumber: 2)
} }
@ -518,9 +557,12 @@ extension RawTransaction: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularBytesField(value: &self.data) case 1: try { try decoder.decodeSingularBytesField(value: &self.data) }()
case 2: try decoder.decodeSingularUInt64Field(value: &self.height) case 2: try { try decoder.decodeSingularUInt64Field(value: &self.height) }()
default: break default: break
} }
} }
@ -553,9 +595,12 @@ extension SendResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularInt32Field(value: &self.errorCode) case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }()
case 2: try decoder.decodeSingularStringField(value: &self.errorMessage) case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }()
default: break default: break
} }
} }
@ -638,21 +683,24 @@ extension LightdInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularStringField(value: &self.version) case 1: try { try decoder.decodeSingularStringField(value: &self.version) }()
case 2: try decoder.decodeSingularStringField(value: &self.vendor) case 2: try { try decoder.decodeSingularStringField(value: &self.vendor) }()
case 3: try decoder.decodeSingularBoolField(value: &self.taddrSupport) case 3: try { try decoder.decodeSingularBoolField(value: &self.taddrSupport) }()
case 4: try decoder.decodeSingularStringField(value: &self.chainName) case 4: try { try decoder.decodeSingularStringField(value: &self.chainName) }()
case 5: try decoder.decodeSingularUInt64Field(value: &self.saplingActivationHeight) case 5: try { try decoder.decodeSingularUInt64Field(value: &self.saplingActivationHeight) }()
case 6: try decoder.decodeSingularStringField(value: &self.consensusBranchID) case 6: try { try decoder.decodeSingularStringField(value: &self.consensusBranchID) }()
case 7: try decoder.decodeSingularUInt64Field(value: &self.blockHeight) case 7: try { try decoder.decodeSingularUInt64Field(value: &self.blockHeight) }()
case 8: try decoder.decodeSingularStringField(value: &self.gitCommit) case 8: try { try decoder.decodeSingularStringField(value: &self.gitCommit) }()
case 9: try decoder.decodeSingularStringField(value: &self.branch) case 9: try { try decoder.decodeSingularStringField(value: &self.branch) }()
case 10: try decoder.decodeSingularStringField(value: &self.buildDate) case 10: try { try decoder.decodeSingularStringField(value: &self.buildDate) }()
case 11: try decoder.decodeSingularStringField(value: &self.buildUser) case 11: try { try decoder.decodeSingularStringField(value: &self.buildUser) }()
case 12: try decoder.decodeSingularUInt64Field(value: &self.estimatedHeight) case 12: try { try decoder.decodeSingularUInt64Field(value: &self.estimatedHeight) }()
case 13: try decoder.decodeSingularStringField(value: &self.zcashdBuild) case 13: try { try decoder.decodeSingularStringField(value: &self.zcashdBuild) }()
case 14: try decoder.decodeSingularStringField(value: &self.zcashdSubversion) case 14: try { try decoder.decodeSingularStringField(value: &self.zcashdSubversion) }()
default: break default: break
} }
} }
@ -733,21 +781,28 @@ extension TransparentAddressBlockFilter: SwiftProtobuf.Message, SwiftProtobuf._M
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularStringField(value: &self.address) case 1: try { try decoder.decodeSingularStringField(value: &self.address) }()
case 2: try decoder.decodeSingularMessageField(value: &self._range) case 2: try { try decoder.decodeSingularMessageField(value: &self._range) }()
default: break default: break
} }
} }
} }
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.address.isEmpty { if !self.address.isEmpty {
try visitor.visitSingularStringField(value: self.address, fieldNumber: 1) try visitor.visitSingularStringField(value: self.address, fieldNumber: 1)
} }
if let v = self._range { try { if let v = self._range {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2) try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} } }()
try unknownFields.traverse(visitor: &visitor) try unknownFields.traverse(visitor: &visitor)
} }
@ -767,8 +822,11 @@ extension Duration: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularInt64Field(value: &self.intervalUs) case 1: try { try decoder.decodeSingularInt64Field(value: &self.intervalUs) }()
default: break default: break
} }
} }
@ -797,9 +855,12 @@ extension PingResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularInt64Field(value: &self.entry) case 1: try { try decoder.decodeSingularInt64Field(value: &self.entry) }()
case 2: try decoder.decodeSingularInt64Field(value: &self.exit) case 2: try { try decoder.decodeSingularInt64Field(value: &self.exit) }()
default: break default: break
} }
} }
@ -831,8 +892,11 @@ extension Address: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularStringField(value: &self.address) case 1: try { try decoder.decodeSingularStringField(value: &self.address) }()
default: break default: break
} }
} }
@ -860,8 +924,11 @@ extension AddressList: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeRepeatedStringField(value: &self.addresses) case 1: try { try decoder.decodeRepeatedStringField(value: &self.addresses) }()
default: break default: break
} }
} }
@ -889,8 +956,11 @@ extension Balance: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularInt64Field(value: &self.valueZat) case 1: try { try decoder.decodeSingularInt64Field(value: &self.valueZat) }()
default: break default: break
} }
} }
@ -918,8 +988,11 @@ extension Exclude: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeRepeatedBytesField(value: &self.txid) case 1: try { try decoder.decodeRepeatedBytesField(value: &self.txid) }()
default: break default: break
} }
} }
@ -951,12 +1024,15 @@ extension TreeState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularStringField(value: &self.network) case 1: try { try decoder.decodeSingularStringField(value: &self.network) }()
case 2: try decoder.decodeSingularUInt64Field(value: &self.height) case 2: try { try decoder.decodeSingularUInt64Field(value: &self.height) }()
case 3: try decoder.decodeSingularStringField(value: &self.hash) case 3: try { try decoder.decodeSingularStringField(value: &self.hash) }()
case 4: try decoder.decodeSingularUInt32Field(value: &self.time) case 4: try { try decoder.decodeSingularUInt32Field(value: &self.time) }()
case 5: try decoder.decodeSingularStringField(value: &self.tree) case 5: try { try decoder.decodeSingularStringField(value: &self.tree) }()
default: break default: break
} }
} }
@ -1002,10 +1078,13 @@ extension GetAddressUtxosArg: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeRepeatedStringField(value: &self.addresses) case 1: try { try decoder.decodeRepeatedStringField(value: &self.addresses) }()
case 2: try decoder.decodeSingularUInt64Field(value: &self.startHeight) case 2: try { try decoder.decodeSingularUInt64Field(value: &self.startHeight) }()
case 3: try decoder.decodeSingularUInt32Field(value: &self.maxEntries) case 3: try { try decoder.decodeSingularUInt32Field(value: &self.maxEntries) }()
default: break default: break
} }
} }
@ -1046,13 +1125,16 @@ extension GetAddressUtxosReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeSingularBytesField(value: &self.txid) case 1: try { try decoder.decodeSingularBytesField(value: &self.txid) }()
case 2: try decoder.decodeSingularInt32Field(value: &self.index) case 2: try { try decoder.decodeSingularInt32Field(value: &self.index) }()
case 3: try decoder.decodeSingularBytesField(value: &self.script) case 3: try { try decoder.decodeSingularBytesField(value: &self.script) }()
case 4: try decoder.decodeSingularInt64Field(value: &self.valueZat) case 4: try { try decoder.decodeSingularInt64Field(value: &self.valueZat) }()
case 5: try decoder.decodeSingularUInt64Field(value: &self.height) case 5: try { try decoder.decodeSingularUInt64Field(value: &self.height) }()
case 6: try decoder.decodeSingularStringField(value: &self.address) case 6: try { try decoder.decodeSingularStringField(value: &self.address) }()
default: break default: break
} }
} }
@ -1100,8 +1182,11 @@ extension GetAddressUtxosReplyList: SwiftProtobuf.Message, SwiftProtobuf._Messag
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() { while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try decoder.decodeRepeatedMessageField(value: &self.addressUtxos) case 1: try { try decoder.decodeRepeatedMessageField(value: &self.addressUtxos) }()
default: break default: break
} }
} }

View File

@ -201,7 +201,7 @@ public protocol Synchronizer {
/// 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 unshielded balance /// Returns the last stored unshielded balance
func getTransparentBalance(accountIndex: Int) throws -> WalletBalance func getTransparentBalance(accountIndex: Int) throws -> WalletBalance

View File

@ -192,7 +192,7 @@ public class SDKSynchronizer: Synchronizer {
return return
} }
blockProcessor.stop(cancelTasks: true) blockProcessor.stop()
self.status = .stopped self.status = .stopped
} }
@ -527,27 +527,18 @@ public class SDKSynchronizer: Synchronizer {
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, toAddress: zAddr, memo: memo, from: 0) let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, toAddress: zAddr, memo: memo, from: 0)
transactionManager.encodeShieldingTransaction( // TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487
spendingKey: spendingKey, Task {
tsk: transparentSecretKey, do {
pendingTransaction: shieldingSpend let transaction = try await transactionManager.encodeShieldingTransaction(
) { [weak self] result in spendingKey: spendingKey,
guard let self = self else { return } tsk: transparentSecretKey,
pendingTransaction: shieldingSpend
switch result { )
case .success(let transaction):
self.transactionManager.submit(pendingTransaction: transaction) { submitResult in
switch submitResult {
case .success(let submittedTx):
resultBlock(.success(submittedTx))
case .failure(let submissionError):
DispatchQueue.main.async {
resultBlock(.failure(submissionError))
}
}
}
case .failure(let error): let submittedTx = try await transactionManager.submit(pendingTransaction: transaction)
resultBlock(.success(submittedTx))
} catch {
resultBlock(.failure(error)) resultBlock(.failure(error))
} }
} }
@ -574,22 +565,16 @@ public class SDKSynchronizer: Synchronizer {
from: accountIndex from: accountIndex
) )
transactionManager.encode(spendingKey: spendingKey, pendingTransaction: spend) { [weak self] result in // TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487
guard let self = self else { return } Task {
switch result { do {
case .success(let transaction): let transaction = try await transactionManager.encode(
self.transactionManager.submit(pendingTransaction: transaction) { submitResult in spendingKey: spendingKey,
switch submitResult { pendingTransaction: spend
case .success(let submittedTx): )
resultBlock(.success(submittedTx)) let submittedTx = try await transactionManager.submit(pendingTransaction: transaction)
case .failure(let submissionError): resultBlock(.success(submittedTx))
DispatchQueue.main.async { } catch {
resultBlock(.failure(submissionError))
}
}
}
case .failure(let error):
resultBlock(.failure(error)) resultBlock(.failure(error))
} }
} }
@ -631,7 +616,14 @@ public class SDKSynchronizer: Synchronizer {
} }
public func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) { public func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) {
blockProcessor.downloader.latestBlockHeight(result: result) Task {
do {
let latestBlockHeight = try await blockProcessor.downloader.latestBlockHeightAsync()
result(.success(latestBlockHeight))
} catch {
result(.failure(error))
}
}
} }
public func latestHeight() throws -> BlockHeight { public func latestHeight() throws -> BlockHeight {
@ -661,8 +653,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 {
@ -878,6 +870,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

@ -67,167 +67,129 @@ class PersistentTransactionManager: OutboundTransactionManager {
func encodeShieldingTransaction( func encodeShieldingTransaction(
spendingKey: String, spendingKey: String,
tsk: String, tsk: String,
pendingTransaction: PendingTransactionEntity, pendingTransaction: PendingTransactionEntity
result: @escaping (Result<PendingTransactionEntity, Error>) -> Void ) async throws -> PendingTransactionEntity {
) { let derivationTool = DerivationTool(networkType: self.network)
queue.async { [weak self] in
guard let self = self else { return } guard
let viewingKey = try? derivationTool.deriveViewingKey(spendingKey: spendingKey),
let derivationTool = DerivationTool(networkType: self.network) let zAddr = try? derivationTool.deriveShieldedAddress(viewingKey: viewingKey)
else {
guard throw TransactionManagerError.shieldingEncodingFailed(
let viewingKey = try? derivationTool.deriveViewingKey(spendingKey: spendingKey), pendingTransaction,
let zAddr = try? derivationTool.deriveShieldedAddress(viewingKey: viewingKey) reason: "There was an error Deriving your keys"
else { )
result( }
.failure(
TransactionManagerError.shieldingEncodingFailed( guard pendingTransaction.toAddress == zAddr else {
pendingTransaction, throw TransactionManagerError.shieldingEncodingFailed(
reason: "There was an error Deriving your keys" pendingTransaction,
) reason: """
) the recipient address does not match your
) derived shielded address. Shielding transactions
return addresses must match the ones derived from your keys.
} This is a serious error. We are not letting you encode
this shielding transaction because it can lead to loss
of funds
"""
)
}
do {
let encodedTransaction = try self.encoder.createShieldingTransaction(
spendingKey: spendingKey,
tSecretKey: tsk,
memo: pendingTransaction.memo?.asZcashTransactionMemo(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
guard pendingTransaction.toAddress == zAddr else { var pending = pendingTransaction
result( pending.encodeAttempts += 1
.failure( pending.raw = encodedTransaction.raw
TransactionManagerError.shieldingEncodingFailed( pending.rawTransactionId = encodedTransaction.transactionId
pendingTransaction, pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
reason: """ pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
the recipient address does not match your
derived shielded address. Shielding transactions try self.repository.update(pending)
addresses must match the ones derived from your keys.
This is a serious error. We are not letting you encode return pending
this shielding transaction because it can lead to loss } catch StorageError.updateFailed {
of funds throw TransactionManagerError.updateFailed(pendingTransaction)
""" } catch {
) throw error
)
)
return
}
do {
let encodedTransaction = try self.encoder.createShieldingTransaction(
spendingKey: spendingKey,
tSecretKey: tsk,
memo: pendingTransaction.memo?.asZcashTransactionMemo(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
var pending = pendingTransaction
pending.encodeAttempts += 1
pending.raw = encodedTransaction.raw
pending.rawTransactionId = encodedTransaction.transactionId
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
try self.repository.update(pending)
result(.success(pending))
} catch StorageError.updateFailed {
DispatchQueue.main.async {
result(.failure(TransactionManagerError.updateFailed(pendingTransaction)))
}
} catch {
DispatchQueue.main.async {
result(.failure(error))
}
}
} }
} }
func encode( func encode(
spendingKey: String, spendingKey: String,
pendingTransaction: PendingTransactionEntity, pendingTransaction: PendingTransactionEntity
result: @escaping (Result<PendingTransactionEntity, Error>) -> Void ) async throws -> PendingTransactionEntity {
) { do {
queue.async { [weak self] in let encodedTransaction = try self.encoder.createTransaction(
guard let self = self else { return } spendingKey: spendingKey,
zatoshi: pendingTransaction.intValue,
to: pendingTransaction.toAddress,
memo: pendingTransaction.memo?.asZcashTransactionMemo(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
var pending = pendingTransaction
pending.encodeAttempts += 1
pending.raw = encodedTransaction.raw
pending.rawTransactionId = encodedTransaction.transactionId
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
try self.repository.update(pending)
return pending
} catch StorageError.updateFailed {
throw TransactionManagerError.updateFailed(pendingTransaction)
} catch {
do { do {
let encodedTransaction = try self.encoder.createTransaction( try self.updateOnFailure(transaction: pendingTransaction, error: error)
spendingKey: spendingKey,
zatoshi: pendingTransaction.intValue,
to: pendingTransaction.toAddress,
memo: pendingTransaction.memo?.asZcashTransactionMemo(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
var pending = pendingTransaction
pending.encodeAttempts += 1
pending.raw = encodedTransaction.raw
pending.rawTransactionId = encodedTransaction.transactionId
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
try self.repository.update(pending)
result(.success(pending))
} catch StorageError.updateFailed {
DispatchQueue.main.async {
result(.failure(TransactionManagerError.updateFailed(pendingTransaction)))
}
} catch { } catch {
do { throw TransactionManagerError.updateFailed(pendingTransaction)
try self.updateOnFailure(transaction: pendingTransaction, error: error)
} catch {
DispatchQueue.main.async {
result(.failure(TransactionManagerError.updateFailed(pendingTransaction)))
}
}
DispatchQueue.main.async {
result(.failure(error))
}
} }
throw error
} }
} }
func submit( func submit(
pendingTransaction: PendingTransactionEntity, pendingTransaction: PendingTransactionEntity
result: @escaping (Result<PendingTransactionEntity, Error>) -> Void ) async throws -> PendingTransactionEntity {
) {
guard let txId = pendingTransaction.id else { guard let txId = pendingTransaction.id else {
result(.failure(TransactionManagerError.notPending(pendingTransaction)))// this transaction is not stored throw TransactionManagerError.notPending(pendingTransaction) // this transaction is not stored
return
} }
queue.async { [weak self] in do {
guard let self = self else { return } guard let storedTx = try self.repository.find(by: txId) else {
throw TransactionManagerError.notPending(pendingTransaction)
do {
guard let storedTx = try self.repository.find(by: txId) else {
result(.failure(TransactionManagerError.notPending(pendingTransaction)))
return
}
guard !storedTx.isCancelled else {
LoggerProxy.debug("ignoring cancelled transaction \(storedTx)")
result(.failure(TransactionManagerError.cancelled(storedTx)))
return
}
guard let raw = storedTx.raw else {
LoggerProxy.debug("INCONSISTENCY: attempt to send pending transaction \(txId) that has not raw data")
result(.failure(TransactionManagerError.internalInconsistency(storedTx)))
return
}
let response = try self.service.submit(spendTransaction: raw)
let transaction = try self.update(transaction: storedTx, on: response)
guard response.errorCode >= 0 else {
result(.failure(TransactionManagerError.submitFailed(transaction, errorCode: Int(response.errorCode))))
return
}
result(.success(transaction))
} catch {
try? self.updateOnFailure(transaction: pendingTransaction, error: error)
result(.failure(error))
} }
guard !storedTx.isCancelled else {
LoggerProxy.debug("ignoring cancelled transaction \(storedTx)")
throw TransactionManagerError.cancelled(storedTx)
}
guard let raw = storedTx.raw else {
LoggerProxy.debug("INCONSISTENCY: attempt to send pending transaction \(txId) that has not raw data")
throw TransactionManagerError.internalInconsistency(storedTx)
}
let response = try self.service.submit(spendTransaction: raw)
let transaction = try self.update(transaction: storedTx, on: response)
guard response.errorCode >= 0 else {
throw TransactionManagerError.submitFailed(transaction, errorCode: Int(response.errorCode))
}
return transaction
} catch {
try? self.updateOnFailure(transaction: pendingTransaction, error: error)
throw error
} }
} }

View File

@ -16,12 +16,12 @@ transactions through to completion.
protocol OutboundTransactionManager { protocol OutboundTransactionManager {
func initSpend(zatoshi: Zatoshi, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity func initSpend(zatoshi: Zatoshi, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity
func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity) async throws -> PendingTransactionEntity
func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity) async throws -> PendingTransactionEntity
func submit(pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) func submit(pendingTransaction: PendingTransactionEntity) async throws -> PendingTransactionEntity
func applyMinedHeight(pendingTransaction: PendingTransactionEntity, minedHeight: BlockHeight) throws -> PendingTransactionEntity func applyMinedHeight(pendingTransaction: PendingTransactionEntity, minedHeight: BlockHeight) throws -> PendingTransactionEntity
func monitorChanges(byId: Int, observer: Any) // check this out. code smell func monitorChanges(byId: Int, observer: Any) // check this out. code smell

View File

@ -40,31 +40,23 @@ class BlockDownloaderTests: XCTestCase {
try? FileManager.default.removeItem(at: cacheDB) try? FileManager.default.removeItem(at: cacheDB)
} }
func testSmallDownloadAsync() { func testSmallDownloadAsync() async {
let expect = XCTestExpectation(description: self.description)
expect.expectedFulfillmentCount = 3
let lowerRange: BlockHeight = self.network.constants.saplingActivationHeight let lowerRange: BlockHeight = self.network.constants.saplingActivationHeight
let upperRange: BlockHeight = self.network.constants.saplingActivationHeight + 99 let upperRange: BlockHeight = self.network.constants.saplingActivationHeight + 99
let range = CompactBlockRange(uncheckedBounds: (lowerRange, upperRange)) let range = CompactBlockRange(uncheckedBounds: (lowerRange, upperRange))
downloader.downloadBlockRange(range) { error in do {
expect.fulfill() try await downloader.downloadBlockRangeAsync(range)
XCTAssertNil(error)
// check what was 'stored' // check what was 'stored'
self.storage.latestHeight { result in let latestHeight = try await self.storage.latestHeightAsync()
expect.fulfill() XCTAssertEqual(latestHeight, upperRange)
XCTAssertTrue(self.validate(result: result, against: upperRange)) let resultHeight = try await self.downloader.lastDownloadedBlockHeightAsync()
XCTAssertEqual(resultHeight, upperRange)
self.downloader.lastDownloadedBlockHeight { resultHeight in } catch {
expect.fulfill() XCTFail("testSmallDownloadAsync() shouldn't fail")
XCTAssertTrue(self.validate(result: resultHeight, against: upperRange))
}
}
} }
wait(for: [expect], timeout: 2)
} }
func testSmallDownload() { func testSmallDownload() {
@ -94,7 +86,7 @@ class BlockDownloaderTests: XCTestCase {
XCTAssertEqual(currentLatest, upperRange ) XCTAssertEqual(currentLatest, upperRange )
} }
func testFailure() { func testFailure() async {
let awfulDownloader = CompactBlockDownloader( let awfulDownloader = CompactBlockDownloader(
service: AwfulLightWalletService( service: AwfulLightWalletService(
latestBlockHeight: self.network.constants.saplingActivationHeight + 1000, latestBlockHeight: self.network.constants.saplingActivationHeight + 1000,
@ -103,18 +95,16 @@ class BlockDownloaderTests: XCTestCase {
storage: ZcashConsoleFakeStorage() storage: ZcashConsoleFakeStorage()
) )
let expect = XCTestExpectation(description: self.description)
expect.expectedFulfillmentCount = 1
let lowerRange: BlockHeight = self.network.constants.saplingActivationHeight let lowerRange: BlockHeight = self.network.constants.saplingActivationHeight
let upperRange: BlockHeight = self.network.constants.saplingActivationHeight + 99 let upperRange: BlockHeight = self.network.constants.saplingActivationHeight + 99
let range = CompactBlockRange(uncheckedBounds: (lowerRange, upperRange)) let range = CompactBlockRange(uncheckedBounds: (lowerRange, upperRange))
awfulDownloader.downloadBlockRange(range) { error in do {
expect.fulfill() try await awfulDownloader.downloadBlockRangeAsync(range)
} catch {
XCTAssertNotNil(error) XCTAssertNotNil(error)
} }
wait(for: [expect], timeout: 2)
} }
} }

View File

@ -109,7 +109,7 @@ final class SynchronizerTests: XCTestCase {
wait(for: [processorStoppedExpectation,syncStoppedExpectation], timeout: 6, enforceOrder: true) wait(for: [processorStoppedExpectation,syncStoppedExpectation], timeout: 6, enforceOrder: true)
XCTAssertEqual(coordinator.synchronizer.status, .stopped) XCTAssertEqual(coordinator.synchronizer.status, .stopped)
XCTAssertEqual(coordinator.synchronizer.blockProcessor.state, .stopped) XCTAssertEqual(coordinator.synchronizer.blockProcessor.state.getState(), .stopped)
} }
func handleError(_ error: Error?) { func handleError(_ error: Error?) {

View File

@ -1,265 +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 uvk = UVFakeKey(
extfvk: "zxviewtestsapling1qw88ayg8qqqqpqyhg7jnh9mlldejfqwu46pm40ruwstd8znq3v3l4hjf33qcu2a5e36katshcfhcxhzgyfugj2lkhmt40j45cv38rv3frnghzkxcx73k7m7afw9j7ujk7nm4dx5mv02r26umxqgar7v3x390w2h3crqqgjsjly7jy4vtwzrmustm5yudpgcydw7x78awca8wqjvkqj8p8e3ykt7lrgd7xf92fsfqjs5vegfsja4ekzpfh5vtccgvs5747xqm6qflmtqpr8s9u", // swiftlint:disable:this line_length
extpub: "02075a7f5f7507d64022dad5954849f216b0f1b09b2d588be663d8e7faeb5aaf61"
)
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)
XCTAssertNoThrow(try rustWelding.initDataDb(dbData: dataDbURL, networkType: network.networkType))
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
)
try self.rustWelding.initDataDb(dbData: dataDbURL, networkType: network.networkType)
guard try self.rustWelding.initAccountsTable(dbData: self.dataDbURL, uvks: [uvk], 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) {
}
}
struct UVFakeKey: UnifiedViewingKey {
var extfvk: ExtendedFullViewingKey
var extpub: ExtendedPublicKey
}

View File

@ -0,0 +1,195 @@
//
// 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 uvk = UVFakeKey(
extfvk: "zxviewtestsapling1qw88ayg8qqqqpqyhg7jnh9mlldejfqwu46pm40ruwstd8znq3v3l4hjf33qcu2a5e36katshcfhcxhzgyfugj2lkhmt40j45cv38rv3frnghzkxcx73k7m7afw9j7ujk7nm4dx5mv02r26umxqgar7v3x390w2h3crqqgjsjly7jy4vtwzrmustm5yudpgcydw7x78awca8wqjvkqj8p8e3ykt7lrgd7xf92fsfqjs5vegfsja4ekzpfh5vtccgvs5747xqm6qflmtqpr8s9u", // swiftlint:disable:this line_length
extpub: "02075a7f5f7507d64022dad5954849f216b0f1b09b2d588be663d8e7faeb5aaf61"
)
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, 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
)
try self.rustWelding.initDataDb(dbData: dataDbURL, networkType: network.networkType)
guard try self.rustWelding.initAccountsTable(dbData: self.dataDbURL, uvks: [uvk], 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)")
}
}
}
}
struct UVFakeKey: UnifiedViewingKey {
var extfvk: ExtendedFullViewingKey
var extpub: ExtendedPublicKey
}

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

@ -61,6 +61,21 @@ class LightWalletServiceTests: XCTestCase {
wait(for: [expect], timeout: 10) wait(for: [expect], timeout: 10)
} }
func testHundredBlocks() async throws {
let count = 99
let lowerRange: BlockHeight = network.constants.saplingActivationHeight
let upperRange: BlockHeight = network.constants.saplingActivationHeight + count
let blockRange = lowerRange ... upperRange
var blocks: [ZcashCompactBlock] = []
for try await block in service.blockRange(blockRange) {
blocks.append(block)
}
XCTAssertEqual(blocks.count, blockRange.count)
XCTAssertEqual(blocks[0].height, lowerRange)
XCTAssertEqual(blocks.last!.height, upperRange)
}
func testSyncBlockRange() { func testSyncBlockRange() {
let lowerRange: BlockHeight = network.constants.saplingActivationHeight let lowerRange: BlockHeight = network.constants.saplingActivationHeight
let upperRange: BlockHeight = network.constants.saplingActivationHeight + 99 let upperRange: BlockHeight = network.constants.saplingActivationHeight + 99
@ -74,6 +89,18 @@ class LightWalletServiceTests: XCTestCase {
} }
} }
func testSyncBlockRange() async throws {
let lowerRange: BlockHeight = network.constants.saplingActivationHeight
let upperRange: BlockHeight = network.constants.saplingActivationHeight + 99
let blockRange = lowerRange ... upperRange
var blocks: [ZcashCompactBlock] = []
for try await block in service.blockRange(blockRange) {
blocks.append(block)
}
XCTAssertEqual(blocks.count, blockRange.count)
}
func testLatestBlock() { func testLatestBlock() {
let expect = XCTestExpectation(description: self.description) let expect = XCTestExpectation(description: self.description)
service.latestBlockHeight { result in service.latestBlockHeight { result in
@ -88,4 +115,9 @@ class LightWalletServiceTests: XCTestCase {
wait(for: [expect], timeout: 10) wait(for: [expect], timeout: 10)
} }
func testLatestBlock() async throws {
let height = try await service.latestBlockHeightAsync()
XCTAssertTrue(height > self.network.constants.saplingActivationHeight)
}
} }

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

@ -19,7 +19,12 @@ class CompactBlockStorageTests: XCTestCase {
func testEmptyStorage() { func testEmptyStorage() {
XCTAssertEqual(try! compactBlockDao.latestHeight(), BlockHeight.empty()) XCTAssertEqual(try! compactBlockDao.latestHeight(), BlockHeight.empty())
} }
func testEmptyStorageAsync() async throws {
let latestHeight = try await compactBlockDao.latestHeightAsync()
XCTAssertEqual(latestHeight, BlockHeight.empty())
}
func testStoreThousandBlocks() { func testStoreThousandBlocks() {
let initialHeight = try! compactBlockDao.latestHeight() let initialHeight = try! compactBlockDao.latestHeight()
let startHeight = self.network.constants.saplingActivationHeight let startHeight = self.network.constants.saplingActivationHeight
@ -38,6 +43,19 @@ class CompactBlockStorageTests: XCTestCase {
XCTAssertEqual(latestHeight, finalHeight) XCTAssertEqual(latestHeight, finalHeight)
} }
func testStoreThousandBlocksAsync() async throws {
let initialHeight = try! compactBlockDao.latestHeight()
let startHeight = self.network.constants.saplingActivationHeight
let blockCount = Int(1_000)
let finalHeight = startHeight + blockCount
try TestDbBuilder.seed(db: compactBlockDao, with: startHeight...finalHeight)
let latestHeight = try await compactBlockDao.latestHeightAsync()
XCTAssertNotEqual(initialHeight, latestHeight)
XCTAssertEqual(latestHeight, finalHeight)
}
func testStoreOneBlockFromEmpty() { func testStoreOneBlockFromEmpty() {
let initialHeight = try! compactBlockDao.latestHeight() let initialHeight = try! compactBlockDao.latestHeight()
guard initialHeight == BlockHeight.empty() else { guard initialHeight == BlockHeight.empty() else {
@ -61,6 +79,24 @@ class CompactBlockStorageTests: XCTestCase {
} }
} }
func testStoreOneBlockFromEmptyAsync() async throws {
let initialHeight = try await compactBlockDao.latestHeightAsync()
guard initialHeight == BlockHeight.empty() else {
XCTFail("database not empty, latest height: \(initialHeight)")
return
}
let expectedHeight = BlockHeight(123_456)
guard let block = StubBlockCreator.createRandomDataBlock(with: expectedHeight) else {
XCTFail("could not create randem block with height: \(expectedHeight)")
return
}
try await compactBlockDao.writeAsync(blocks: [block])
let result = try await compactBlockDao.latestHeightAsync()
XCTAssertEqual(result, expectedHeight)
}
func testRewindTo() { func testRewindTo() {
let startHeight = self.network.constants.saplingActivationHeight let startHeight = self.network.constants.saplingActivationHeight
let blockCount = Int(1_000) let blockCount = Int(1_000)
@ -82,4 +118,17 @@ class CompactBlockStorageTests: XCTestCase {
XCTFail("Rewind latest block failed with error: \(error)") XCTFail("Rewind latest block failed with error: \(error)")
} }
} }
func testRewindToAsync() async throws {
let startHeight = self.network.constants.saplingActivationHeight
let blockCount = Int(1_000)
let finalHeight = startHeight + blockCount
try TestDbBuilder.seed(db: compactBlockDao, with: startHeight...finalHeight)
let rewindHeight = BlockHeight(finalHeight - 233)
try await compactBlockDao.rewindAsync(to: rewindHeight)
let latestHeight = try await compactBlockDao.latestHeightAsync()
XCTAssertEqual(latestHeight, rewindHeight - 1)
}
} }

View File

@ -75,6 +75,10 @@ class DarksideWalletService: LightWalletService {
) )
} }
func blockStream(startHeight: BlockHeight, endHeight: BlockHeight) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
service.blockStream(startHeight: startHeight, endHeight: endHeight)
}
func getInfo() throws -> LightWalletdInfo { func getInfo() throws -> LightWalletdInfo {
try service.getInfo() try service.getInfo()
} }
@ -87,7 +91,11 @@ class DarksideWalletService: LightWalletService {
} }
func fetchUTXOs(for tAddress: String, height: BlockHeight) throws -> [UnspentTransactionOutputEntity] { func fetchUTXOs(for tAddress: String, height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
return [] try service.fetchUTXOs(for: tAddress, height: height)
}
func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
try service.fetchUTXOs(for: tAddresses, height: height)
} }
func fetchUTXOs( func fetchUTXOs(
@ -98,8 +106,8 @@ class DarksideWalletService: LightWalletService {
service.fetchUTXOs(for: tAddress, height: height, result: result) service.fetchUTXOs(for: tAddress, height: height, result: result)
} }
func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] { func fetchUTXOs(for tAddress: String, height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
try service.fetchUTXOs(for: tAddresses, height: height) service.fetchUTXOs(for: tAddress, height: height)
} }
func fetchUTXOs( func fetchUTXOs(
@ -109,7 +117,10 @@ class DarksideWalletService: LightWalletService {
) { ) {
service.fetchUTXOs(for: tAddresses, height: height, result: result) service.fetchUTXOs(for: tAddresses, height: height, result: result)
} }
func fetchUTXOs(for tAddresses: [String], height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
service.fetchUTXOs(for: tAddresses, height: height)
}
func fetchTransaction(txId: Data) throws -> TransactionEntity { func fetchTransaction(txId: Data) throws -> TransactionEntity {
try service.fetchTransaction(txId: txId) try service.fetchTransaction(txId: txId)
@ -222,6 +233,26 @@ class DarksideWalletService: LightWalletService {
func clearAddedUTXOs() throws { func clearAddedUTXOs() throws {
_ = try darksideService.clearAddressUtxo(Empty(), callOptions: nil).response.wait() _ = try darksideService.clearAddressUtxo(Empty(), callOptions: nil).response.wait()
} }
func getInfoAsync() async throws -> LightWalletdInfo {
try service.getInfo()
}
func latestBlockHeightAsync() async throws -> BlockHeight {
try service.latestBlockHeight()
}
func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
service.blockRange(range)
}
func submitAsync(spendTransaction: Data) async throws -> LightWalletServiceResponse {
try service.submit(spendTransaction: spendTransaction)
}
func fetchTransactionAsync(txId: Data) async throws -> TransactionEntity {
try service.fetchTransaction(txId: txId)
}
} }
enum DarksideWalletDConstants: NetworkConstants { enum DarksideWalletDConstants: NetworkConstants {

View File

@ -35,6 +35,10 @@ class MockLightWalletService: LightWalletService {
return MockCancellable() return MockCancellable()
} }
func blockStream(startHeight: BlockHeight, endHeight: BlockHeight) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
AsyncThrowingStream { _ in }
}
func getInfo() throws -> LightWalletdInfo { func getInfo() throws -> LightWalletdInfo {
guard let info = mockLightDInfo else { guard let info = mockLightDInfo else {
throw LightWalletServiceError.generalError(message: "Not Implemented") throw LightWalletServiceError.generalError(message: "Not Implemented")
@ -59,28 +63,30 @@ class MockLightWalletService: LightWalletService {
[] []
} }
func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] {
[]
}
func fetchUTXOs( func fetchUTXOs(
for tAddress: String, for tAddress: String,
height: BlockHeight, height: BlockHeight,
result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
) { ) {
} }
func fetchUTXOs(for tAddresses: [String], height: BlockHeight) throws -> [UnspentTransactionOutputEntity] { func fetchUTXOs(for tAddress: String, height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
[] AsyncThrowingStream { _ in }
} }
func fetchUTXOs( func fetchUTXOs(
for tAddresses: [String], for tAddresses: [String],
height: BlockHeight, height: BlockHeight,
result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
) { ) {
} }
func fetchUTXOs( func fetchUTXOs(for tAddresses: [String], height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error> {
for tAddress: String, AsyncThrowingStream { _ in }
result: @escaping (Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void
) {
} }
private var service: LightWalletService private var service: LightWalletService
@ -126,4 +132,35 @@ class MockLightWalletService: LightWalletService {
func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void) { func fetchTransaction(txId: Data, result: @escaping (Result<TransactionEntity, LightWalletServiceError>) -> Void) {
} }
func getInfoAsync() async throws -> LightWalletdInfo {
guard let info = mockLightDInfo else {
throw LightWalletServiceError.generalError(message: "Not Implemented")
}
return info
}
func latestBlockHeightAsync() async throws -> BlockHeight {
latestHeight
}
func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
service.blockRange(range)
}
func submitAsync(spendTransaction: Data) async throws -> LightWalletServiceResponse {
LightWalletServiceMockResponse(errorCode: 0, errorMessage: "", unknownFields: UnknownStorage())
}
func fetchTransactionAsync(txId: Data) async throws -> TransactionEntity {
Transaction(id: 1, transactionId: Data(), created: "Today", transactionIndex: 1, expiryHeight: -1, minedHeight: -1, raw: nil)
}
func fetchUTXOsAsync(for tAddress: String, height: BlockHeight) async throws -> [UnspentTransactionOutputEntity] {
[]
}
func fetchUTXOsAsync(for tAddresses: [String], height: BlockHeight) async throws -> [UnspentTransactionOutputEntity] {
[]
}
} }

View File

@ -10,6 +10,18 @@ import Foundation
@testable import ZcashLightClientKit @testable import ZcashLightClientKit
class ZcashConsoleFakeStorage: CompactBlockRepository { class ZcashConsoleFakeStorage: CompactBlockRepository {
func latestHeightAsync() async throws -> BlockHeight {
latestBlockHeight
}
func writeAsync(blocks: [ZcashCompactBlock]) async throws {
fakeSave(blocks: blocks)
}
func rewindAsync(to height: BlockHeight) async throws {
fakeRewind(to: height)
}
func latestHeight() throws -> Int { func latestHeight() throws -> Int {
return self.latestBlockHeight return self.latestBlockHeight
} }
@ -29,12 +41,6 @@ class ZcashConsoleFakeStorage: CompactBlockRepository {
self.latestBlockHeight = latestBlockHeight self.latestBlockHeight = latestBlockHeight
} }
func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
result(.success(self.latestBlockHeight))
}
}
private func fakeSave(blocks: [ZcashCompactBlock]) { private func fakeSave(blocks: [ZcashCompactBlock]) {
blocks.forEach { blocks.forEach {
LoggerProxy.debug("saving block \($0)") LoggerProxy.debug("saving block \($0)")
@ -42,20 +48,6 @@ class ZcashConsoleFakeStorage: CompactBlockRepository {
} }
} }
func write(blocks: [ZcashCompactBlock], completion: ((Error?) -> Void)?) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.fakeSave(blocks: blocks)
completion?(nil)
}
}
func rewind(to height: BlockHeight, completion: ((Error?) -> Void)?) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.fakeRewind(to: height)
completion?(nil)
}
}
private func fakeRewind(to height: BlockHeight) { private func fakeRewind(to height: BlockHeight) {
LoggerProxy.debug("rewind to \(height)") LoggerProxy.debug("rewind to \(height)")
self.latestBlockHeight = min(self.latestBlockHeight, height) self.latestBlockHeight = min(self.latestBlockHeight, height)

View File

@ -33,6 +33,10 @@ class AwfulLightWalletService: MockLightWalletService {
} }
} }
override func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
AsyncThrowingStream { continuation in continuation.finish(throwing: LightWalletServiceError.invalidBlock) }
}
override func submit(spendTransaction: Data, result: @escaping(Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) { override func submit(spendTransaction: Data, result: @escaping(Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
result(.failure(LightWalletServiceError.invalidBlock)) result(.failure(LightWalletServiceError.invalidBlock))