diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift index 052261db..919bdef1 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift @@ -45,24 +45,17 @@ class GetUTXOsViewController: UIViewController { KRProgressHUD.showMessage("🛡 Shielding 🛡") - AppDelegate.shared.sharedSynchronizer.shieldFunds( - transparentAccountPrivateKey: transparentSecretKey, - memo: "shielding is fun!", - from: 0, - resultBlock: { result in - DispatchQueue.main.async { - KRProgressHUD.dismiss() - switch result { - case .success(let transaction): - self.messageLabel.text = "funds shielded \(transaction)" - case .failure(let error): - self.messageLabel.text = "Shielding failed: \(error)" - } - } - } - ) + Task { @MainActor in + let transaction = try await AppDelegate.shared.sharedSynchronizer.shieldFunds( + transparentAccountPrivateKey: transparentSecretKey, + memo: "shielding is fun!", + from: 0 + ) + KRProgressHUD.dismiss() + self.messageLabel.text = "funds shielded \(transaction)" + } } catch { - self.messageLabel.text = "Error \(error)" + self.messageLabel.text = "Shielding failed \(error)" } } } diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sapling Parameters/SaplingParametersViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sapling Parameters/SaplingParametersViewController.swift index 51aff80c..ad9d9812 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Sapling Parameters/SaplingParametersViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sapling Parameters/SaplingParametersViewController.swift @@ -79,25 +79,21 @@ class SaplingParametersViewController: UIViewController { @IBAction func download(_ sender: Any) { let outputParameter = try! outputParamsURLHelper() let spendParameter = try! spendParamsURLHelper() - SaplingParameterDownloader.downloadParamsIfnotPresent( - spendURL: spendParameter, - outputURL: outputParameter, - result: { result in - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - switch result { - case .success(let urls): - self.spendPath.text = urls.spend.path - self.outputPath.text = urls.output.path - self.updateColor() - self.updateButtons() - - case .failure(let error): - self.showError(error) - } - } + + Task { @MainActor in + do { + let urls = try await SaplingParameterDownloader.downloadParamsIfnotPresent( + spendURL: spendParameter, + outputURL: outputParameter + ) + spendPath.text = urls.spend.path + outputPath.text = urls.output.path + updateColor() + updateButtons() + } catch { + showError(error) } - ) + } } func fileExists(_ path: String) -> Bool { diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Send/SendViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Send/SendViewController.swift index 38b6b43e..09b978d4 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Send/SendViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Send/SendViewController.swift @@ -220,28 +220,21 @@ class SendViewController: UIViewController { } KRProgressHUD.show() - - synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: zec, - // swiftlint:disable:next force_try - toAddress: try! Recipient(recipient, network: kZcashNetwork.networkType), - memo: !self.memoField.text.isEmpty ? self.memoField.text : nil, - from: 0 - ) { [weak self] result in - DispatchQueue.main.async { + + Task { @MainActor in + do { + let pendingTransaction = try await synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: zec, + toAddress: recipient, + memo: !self.memoField.text.isEmpty ? self.memoField.text : nil, + from: 0 + ) KRProgressHUD.dismiss() - } - - switch result { - case .success(let pendingTransaction): loggerProxy.info("transaction created: \(pendingTransaction)") - - case .failure(let error): - DispatchQueue.main.async { - self?.fail(error) - loggerProxy.error("SEND FAILED: \(error)") - } + } catch { + fail(error) + loggerProxy.error("SEND FAILED: \(error)") } } } diff --git a/Sources/ZcashLightClientKit/Block/Processor/CompactBlockDownload.swift b/Sources/ZcashLightClientKit/Block/Processor/CompactBlockDownload.swift index 58eefb02..db6cd260 100644 --- a/Sources/ZcashLightClientKit/Block/Processor/CompactBlockDownload.swift +++ b/Sources/ZcashLightClientKit/Block/Processor/CompactBlockDownload.swift @@ -29,6 +29,7 @@ extension CompactBlockProcessor { guard let latestHeight = targetHeightInternal else { throw LightWalletServiceError.generalError(message: "missing target height on compactBlockStreamDownload") } + try Task.checkCancellation() let latestDownloaded = try await storage.latestHeightAsync() let startHeight = max(startHeight ?? BlockHeight.empty(), latestDownloaded) @@ -38,6 +39,7 @@ extension CompactBlockProcessor { ) for try await zcashCompactBlock in stream { + try Task.checkCancellation() 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 diff --git a/Sources/ZcashLightClientKit/Initializer.swift b/Sources/ZcashLightClientKit/Initializer.swift index da2fa7fb..9e47b03c 100644 --- a/Sources/ZcashLightClientKit/Initializer.swift +++ b/Sources/ZcashLightClientKit/Initializer.swift @@ -329,48 +329,35 @@ public class Initializer { FileManager.default.isReadableFile(atPath: self.outputParamsURL.path) } - func downloadParametersIfNeeded(result: @escaping (Result) -> Void) { + @discardableResult + func downloadParametersIfNeeded() async throws -> Bool { let spendParameterPresent = isSpendParameterPresent() let outputParameterPresent = isOutputParameterPresent() if spendParameterPresent && outputParameterPresent { - result(.success(true)) - return + return true } let outputURL = self.outputParamsURL let spendURL = self.spendParamsURL - if !outputParameterPresent { - SaplingParameterDownloader.downloadOutputParameter(outputURL) { outputResult in - switch outputResult { - case .failure(let error): - result(.failure(error)) - case .success: - guard !spendParameterPresent else { - result(.success(false)) - return - } - SaplingParameterDownloader.downloadSpendParameter(spendURL) { spendResult in - switch spendResult { - case .failure(let error): - result(.failure(error)) - case .success: - result(.success(false)) - } - } - } - } - } else if !spendParameterPresent { - SaplingParameterDownloader.downloadSpendParameter(spendURL) { spendResult in - switch spendResult { - case .failure(let error): - result(.failure(error)) - case .success: - result(.success(false)) - } + do { + if !outputParameterPresent && !spendParameterPresent { + async let outputURLRequest = SaplingParameterDownloader.downloadOutputParameter(outputURL) + async let spendURLRequest = SaplingParameterDownloader.downloadSpendParameter(spendURL) + _ = try await [outputURLRequest, spendURLRequest] + return false + } else if !outputParameterPresent { + try await SaplingParameterDownloader.downloadOutputParameter(outputURL) + return false + } else if !spendParameterPresent { + try await SaplingParameterDownloader.downloadSpendParameter(spendURL) + return false } + } catch { + throw error } + return true } } diff --git a/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1807500.json b/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1807500.json new file mode 100644 index 00000000..902d00ec --- /dev/null +++ b/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1807500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "1807500", + "hash": "0000000000b92234ffe6efd360a2fb1528e2f5cf891a54e58a9b83cd6c513ad9", + "time": 1663095814, + "saplingTree": "0146b9cd1dc80b44c19108d53ecf72b5bacd405e8ed5c8038c413ad2d7bf9f486201fb61737c80f0c3816ddc487ff9ba360c60b1663151cbd07c08dd8c09c648760d190177aa27c08a1ff4754710984f2f94c5a101a10f57eb6ad49f7b1df47f7e8a5730014c0fb4e36c1b7968082d0355f318a67e265e5a5533ffcc01b939e4bf1779841b000000019315fe44f132446949cd9ea1f34ec20f32c0dd0ba4f732e563c040c692fd5d49010e6b6ae8956bd722b0f5e3f355adfc7c9b6a39542c07fb0bcd317d0eff8d99080001382db97fabb726a88e7a7baab050a1c9169d2142a9ce0b7f3022349acf74981a0175d44cdd04e213c2d95fb281f96fc7d734c65be70a77b7aa4fef8a4b6fb2475b015fe654f11132fc9c133b46dbbf19b0113cc45715f52dfd3282ede76f6880740c01d14f83f0fd7d09f4f52c8ae9d39c63b57c59a37666d211b9a2deb290808c02720001701df279d9a2270a82379486df546fafeaeb831993cde1cd9e5c9cb17be5191f01cc6ae86e9147b0b1c3f5fb32fb7acc012b4cb4384a1a1331ae3c2324a804483e00017fb2e2890b05355ba797af2f77e38cab3e8ec1623d29a912bdd0dc4a78cf554a0001de17a599d0c6d73eaf1a5939e95af4427f75b89f703749e00d853cce3d6af84f00014f6313837ed19d2b480ec531529fb6b425006f2c1d981077640be21627659410018c2d6adea2ad4faf20eccfc2c2a2c59192fb53d3204b3a2757f1c247dadec16b0001c5d9822e7aa76d3a758e8dc25ffd8b8c9d4051c66fb92fd7aa263905e238920e0139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e", + "orchardTree": "013de3a4ad28b5df77a979fc925643e69b7dd26ba787a3122fcd6a445c47d9280e01b68287983d323c90e6a1ed5980ab5b1a846d49340ee6d40d2349795c132a382c1f0001c2db91c5b9baa1c09623429cb4005ae12e521a018eb8df2d051d6793a307eb3e01c6b2f8d93635310d470d4a6d011ea77f59e28bee6ffca3df88f5f2a98980331a01777860ffe739b8047045e2dff8ba77070666075214a0f7702568205410351b39019ed7c5c3e958cb8b9d5324c290ff384dc3ca6cbc870950002f64398478ff1904000114b5ad56c8f210854a1688f47116b5d272fea09559646cee33ad3e6958306a15000001110e689714d772b170f63bfbf4b144c6a48e0a57c4a2780513633d5c799a0d1300010cf94eaf4d5268d9e0878064a458eaa3363dfcfcf2602681c443baf989d5de20000000019776dec2ea06cc5ecd2d212d37023972f526cb2ffa7ce1e8cf8eb4ef04700b01000001c7146e487b3ae97b190ebf93eac554968e683d31115d13fe83dd620859d9a92d000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000" +} diff --git a/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1810000.json b/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1810000.json new file mode 100644 index 00000000..e1edd90b --- /dev/null +++ b/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1810000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "1810000", + "hash": "0000000000bdf01be068ff1f0b21b8b266f839b31ed066b72888b67f672c9800", + "time": 1663283618, + "saplingTree": "019dbc466ad114f2b4a6d0d91198c0a2c5c20c1dec7c2e7f932c9f9197d76b80020019015b272118101cac0ee6b9b8cf26d5104ab42913d2f5253388bac28e7998f2c41b0001868d5008c587f0fa3d26fc42097d34df49a70508a85a6acccf1263d39cb62c28000001b2325a6ecbf023f3ce4b74c1c14bac8d9c462560dce9611bd7b08cd55ecf4b0100000001cd62e4e2142c664656f956fd0ea8d1de1027c327580398fe8912e6264801650201ccc9b305f6e65d641a4e461ea5e853354a3ac4b2acf2591849e00421bd58fb48000000013a591632f71e1fe214ab46b464112520e98f15d171da599a350f7d8dba79595d018d254447626cf40828102a60e2b433d05498a780599cdf56a14f3888c2f42008000148af2e64d92d1944a451180f1738c9f468f608525b0273967db19029b53ba16d0000000001f416eb7e062c981dbbf76f8845fda959b948bc742fc62d9edb2f36bae852ba4e01c5d9822e7aa76d3a758e8dc25ffd8b8c9d4051c66fb92fd7aa263905e238920e0139af7ec003ac4e37c263bab5642fcc4ec294a1e328ff025832eec45239041f6e", + "orchardTree": "0180c286284cd52360af960fce56fe7dc339667c35580c75e9bc339483230e932701260b8984058125b5e3558fb65172d59718427111cbc06891017cc7633f74e1001f00000000000001cde58ba8e982a34499406e06a763ede11353d39ab93a5af1b34905cf268c033701226087d6a9bb28c2ed6890b989224791093cd5012a27285040025c0a72b2ce06000001ba71f8a1897b754e9fe37a29eb2c1a93ddc1678298498b4a84da732ebf056f15018073f4aff677a24eaac68c20d271ea228041f1e77710b0504d3f6c0b71d63d24000146b37a3e6167ae7f07725ab4e32247619c37e2a91c87182dd68b8feb99d5a22201e18dad85447ef2e9b8d647c9b9f1e6cef3e1d03f908975cd5e1d5c5808e443010001898b4a8f384f342a67efb3f6c4afd87310df4ff1532b86ca8d1394975aab5a1e0001c7146e487b3ae97b190ebf93eac554968e683d31115d13fe83dd620859d9a92d000001020d51b05be2e276efa73f2977f5a82defed553b1086897f0bea9b03f4dbd41a000000000000000000" +} diff --git a/Sources/ZcashLightClientKit/Synchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer.swift index d9003107..d54cba96 100644 --- a/Sources/ZcashLightClientKit/Synchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer.swift @@ -104,22 +104,20 @@ public protocol Synchronizer { /// - Parameter accountIndex: the optional accountId whose address is of interest. By default, the first account is used. /// - Returns the address or nil if account index is incorrect func getTransparentAddress(accountIndex: Int) -> TransparentAddress? - + /// Sends zatoshi. /// - Parameter spendingKey: the key that allows spends to occur. /// - Parameter zatoshi: the amount to send in Zatoshi. /// - Parameter toAddress: the recipient's address. /// - Parameter memo: the memo to include as part of the transaction. /// - Parameter accountIndex: the optional account id to use. By default, the first account is used. - // swiftlint:disable:next function_parameter_count func sendToAddress( spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, toAddress: Recipient, memo: Memo, - from accountIndex: Int, - resultBlock: @escaping (_ result: Result) -> Void - ) + from accountIndex: Int + ) async throws -> PendingTransactionEntity /// Shields transparent funds from the given private key into the best shielded pool of the given account. /// - Parameter transparentAccountPrivateKey: the key that allows to spend transparent funds @@ -128,9 +126,8 @@ public protocol Synchronizer { func shieldFunds( transparentAccountPrivateKey: TransparentAccountPrivKey, memo: Memo, - from accountIndex: Int, - resultBlock: @escaping (Result) -> Void - ) + from accountIndex: Int + ) async throws -> PendingTransactionEntity /// Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only /// an option if the transaction has not yet been submitted to the server. diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index e917af78..713b716f 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -77,7 +77,6 @@ public extension Notification.Name { /// Synchronizer implementation for UIKit and iOS 13+ // swiftlint:disable type_body_length public class SDKSynchronizer: Synchronizer { - public struct SynchronizerState { public var shieldedBalance: WalletBalance public var transparentBalance: WalletBalance @@ -457,41 +456,34 @@ public class SDKSynchronizer: Synchronizer { } // MARK: Synchronizer methods - - // swiftlint:disable:next function_parameter_count + public func sendToAddress( spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, toAddress: Recipient, memo: Memo, - from accountIndex: Int, - resultBlock: @escaping (Result) -> Void - ) { - initializer.downloadParametersIfNeeded { downloadResult in - DispatchQueue.main.async { [weak self] in - switch downloadResult { - case .success: - self?.createToAddress( - spendingKey: spendingKey, - zatoshi: zatoshi, - toAddress: toAddress.stringEncoded, - memo: memo, - from: accountIndex, - resultBlock: resultBlock - ) - case .failure(let error): - resultBlock(.failure(SynchronizerError.parameterMissing(underlyingError: error))) - } - } + from accountIndex: Int + ) async throws -> PendingTransactionEntity { + do { + try await initializer.downloadParametersIfNeeded() + } catch { + throw SynchronizerError.parameterMissing(underlyingError: error) } - } + return try await createToAddress( + spendingKey: spendingKey, + zatoshi: zatoshi, + toAddress: toAddress.stringEncoded, + memo: memo, + from: accountIndex + ) + } + public func shieldFunds( transparentAccountPrivateKey: TransparentAccountPrivKey, memo: Memo, - from accountIndex: Int, - resultBlock: @escaping (Result) -> Void - ) { + from accountIndex: Int + ) async throws -> PendingTransactionEntity { // let's see if there are funds to shield let derivationTool = DerivationTool(networkType: self.network.networkType) @@ -502,47 +494,36 @@ public class SDKSynchronizer: Synchronizer { // Verify that at least there are funds for the fee. Ideally this logic will be improved by the shielding wallet. guard tBalance.verified >= self.network.constants.defaultFee(for: self.latestScannedHeight) else { - resultBlock(.failure(ShieldFundsError.insuficientTransparentFunds)) - return + throw ShieldFundsError.insuficientTransparentFunds } // FIXME: Define who's the recipient of a shielding transaction #521 // https://github.com/zcash/ZcashLightClientKit/issues/521 guard let uAddr = self.getUnifiedAddress(accountIndex: accountIndex) else { - resultBlock(.failure(ShieldFundsError.shieldingFailed(underlyingError: KeyEncodingError.invalidEncoding))) - return + throw ShieldFundsError.shieldingFailed(underlyingError: KeyEncodingError.invalidEncoding) } let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, toAddress: uAddr.stringEncoded, memo: try memo.asMemoBytes(), from: accountIndex) // TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487 - Task { - do { - let transaction = try await transactionManager.encodeShieldingTransaction( - xprv: transparentAccountPrivateKey, - pendingTransaction: shieldingSpend - ) + let transaction = try await transactionManager.encodeShieldingTransaction( + xprv: transparentAccountPrivateKey, + pendingTransaction: shieldingSpend + ) - let submittedTx = try await transactionManager.submit(pendingTransaction: transaction) - resultBlock(.success(submittedTx)) - } catch { - resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) - } - } + return try await transactionManager.submit(pendingTransaction: transaction) } catch { - resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) + throw error } } - // swiftlint:disable:next function_parameter_count func createToAddress( spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, toAddress: String, memo: Memo, - from accountIndex: Int, - resultBlock: @escaping (Result) -> Void - ) { + from accountIndex: Int + ) async throws -> PendingTransactionEntity { do { let spend = try transactionManager.initSpend( zatoshi: zatoshi, @@ -551,21 +532,14 @@ public class SDKSynchronizer: Synchronizer { from: accountIndex ) - // TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487 - Task { - do { - let transaction = try await transactionManager.encode( - spendingKey: spendingKey, - pendingTransaction: spend - ) - let submittedTx = try await transactionManager.submit(pendingTransaction: transaction) - resultBlock(.success(submittedTx)) - } catch { - resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) - } - } + let transaction = try await transactionManager.encode( + spendingKey: spendingKey, + pendingTransaction: spend + ) + let submittedTx = try await transactionManager.submit(pendingTransaction: transaction) + return submittedTx } catch { - resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) + throw error } } diff --git a/Sources/ZcashLightClientKit/Utils/SaplingParameterDownloader.swift b/Sources/ZcashLightClientKit/Utils/SaplingParameterDownloader.swift index 432d47b0..cc9b0e5b 100644 --- a/Sources/ZcashLightClientKit/Utils/SaplingParameterDownloader.swift +++ b/Sources/ZcashLightClientKit/Utils/SaplingParameterDownloader.swift @@ -15,33 +15,46 @@ public enum SaplingParameterDownloader { case failed(error: Error) } - /** - Download a Spend parameter from default host and stores it at given URL - - Parameters: - - at: The destination URL for the download - - result: block to handle the download success or error - */ - public static func downloadSpendParameter(_ at: URL, result: @escaping (Result) -> Void) { + /// Download a Spend parameter from default host and stores it at given URL + /// - Parameters: + /// - at: The destination URL for the download + @discardableResult + public static func downloadSpendParameter(_ at: URL) async throws -> URL { guard let url = URL(string: spendParamsURLString) else { - result(.failure(Errors.invalidURL(url: spendParamsURLString))) - return + throw Errors.invalidURL(url: spendParamsURLString) } - downloadFileWithRequest(URLRequest(url: url), at: at, result: result) + return try await withCheckedThrowingContinuation { continuation in + downloadFileWithRequest(URLRequest(url: url), at: at) { result in + switch result { + case .success(let outputResultURL): + continuation.resume(returning: outputResultURL) + case .failure(let outputResultError): + continuation.resume(throwing: outputResultError) + } + } + } } - /** - Download an Output parameter from default host and stores it at given URL - - Parameters: - - at: The destination URL for the download - - result: block to handle the download success or error - */ - public static func downloadOutputParameter(_ at: URL, result: @escaping (Result) -> Void) { + + /// Download an Output parameter from default host and stores it at given URL + /// - Parameters: + /// - at: The destination URL for the download + @discardableResult + public static func downloadOutputParameter(_ at: URL) async throws -> URL { guard let url = URL(string: outputParamsURLString) else { - result(.failure(Errors.invalidURL(url: outputParamsURLString))) - return + throw Errors.invalidURL(url: outputParamsURLString) } - downloadFileWithRequest(URLRequest(url: url), at: at, result: result) + return try await withCheckedThrowingContinuation { continuation in + downloadFileWithRequest(URLRequest(url: url), at: at) { result in + switch result { + case .success(let outputResultURL): + continuation.resume(returning: outputResultURL) + case .failure(let outputResultError): + continuation.resume(throwing: outputResultError) + } + } + } } private static func downloadFileWithRequest(_ request: URLRequest, at destination: URL, result: @escaping (Result) -> Void) { @@ -61,52 +74,39 @@ public enum SaplingParameterDownloader { task.resume() } - /** - Downloads the parameters if not present and provides the resulting URLs for both parameters - - Parameters: - - spendURL: URL to check whether the parameter is already downloaded - - outputURL: URL to check whether the parameter is already downloaded - - result: block to handle success or error - */ + + /// Downloads the parameters if not present and provides the resulting URLs for both parameters + /// - Parameters: + /// - spendURL: URL to check whether the parameter is already downloaded + /// - outputURL: URL to check whether the parameter is already downloaded public static func downloadParamsIfnotPresent( spendURL: URL, - outputURL: URL, - result: @escaping (Result<(spend: URL, output: URL), Error>) -> Void - ) { - ensureSpendParameter(at: spendURL) { spendResult in - switch spendResult { - case .success(let spendResultURL): - ensureOutputParameter(at: outputURL) { outputResult in - switch outputResult { - case .success(let outputResultURL): - result(.success((spendResultURL, outputResultURL))) - case .failure(let outputResultError): - result(.failure(Errors.failed(error: outputResultError))) - } - } - case .failure(let spendResultError): - result(.failure(Errors.failed(error: spendResultError))) - } + outputURL: URL + ) async throws -> (spend: URL, output: URL) { + do { + async let spendResultURL = ensureSpendParameter(at: spendURL) + async let outputResultURL = ensureOutputParameter(at: outputURL) + + let results = try await [spendResultURL, outputResultURL] + return (spend: results[0], output: results[1]) + } catch { + throw Errors.failed(error: error) } } - static func ensureSpendParameter(at url: URL, result: @escaping (Result) -> Void) { + static func ensureSpendParameter(at url: URL) async throws -> URL { if isFilePresent(url: url) { - DispatchQueue.global().async { - result(.success(url)) - } + return url } else { - downloadSpendParameter(url, result: result) + return try await downloadSpendParameter(url) } } - static func ensureOutputParameter(at url: URL, result: @escaping (Result) -> Void) { + static func ensureOutputParameter(at url: URL) async throws -> URL { if isFilePresent(url: url) { - DispatchQueue.global().async { - result(.success(url)) - } + return url } else { - downloadOutputParameter(url, result: result) + return try await downloadOutputParameter(url) } } diff --git a/Tests/DarksideTests/AdvancedReOrgTests.swift b/Tests/DarksideTests/AdvancedReOrgTests.swift index d5eccce9..fe623af5 100644 --- a/Tests/DarksideTests/AdvancedReOrgTests.swift +++ b/Tests/DarksideTests/AdvancedReOrgTests.swift @@ -10,6 +10,7 @@ import XCTest @testable import ZcashLightClientKit // swiftlint:disable implicitly_unwrapped_optional force_unwrapping type_body_length +//@MainActor class AdvancedReOrgTests: XCTestCase { // TODO: Parameterize this from environment? // swiftlint:disable:next line_length @@ -266,7 +267,7 @@ class AdvancedReOrgTests: XCTestCase { /// 11. verify that the sent tx is mined and balance is correct /// 12. applyStaged(sentTx + 10) /// 13. verify that there's no more pending transaction - func testReorgChangesOutboundTxIndex() throws { + func testReorgChangesOutboundTxIndex() async throws { try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName) let receivedTxHeight: BlockHeight = 663188 var initialTotalBalance = Zatoshi(-1) @@ -278,44 +279,51 @@ class AdvancedReOrgTests: XCTestCase { sleep(2) let preTxExpectation = XCTestExpectation(description: "pre receive") - + /* 3. sync up to received_Tx_height */ - try coordinator.sync(completion: { synchronizer in - initialTotalBalance = synchronizer.initializer.getBalance() - preTxExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + initialTotalBalance = synchronizer.initializer.getBalance() + continuation.resume() + preTxExpectation.fulfill() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [preTxExpectation], timeout: 5) let sendExpectation = XCTestExpectation(description: "sendToAddress") var pendingEntity: PendingTransactionEntity? - var error: Error? + var testError: Error? let sendAmount = Zatoshi(10000) /* 4. create transaction */ - coordinator.synchronizer.sendToAddress( - spendingKey: coordinator.spendingKeys!.first!, - zatoshi: sendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "test transaction"), - from: 0 - ) { result in - switch result { - case .success(let pending): - pendingEntity = pending - case .failure(let e): - error = e - } + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: coordinator.spendingKeys!.first!, + zatoshi: sendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "test transaction"), + from: 0 + ) + pendingEntity = pendingTx sendExpectation.fulfill() - } - wait(for: [sendExpectation], timeout: 12) - - guard let pendingTx = pendingEntity else { + } catch { + testError = error XCTFail("error sending to address. Error: \(String(describing: error))") + } + + wait(for: [sendExpectation], timeout: 2) + + guard let pendingTx = pendingEntity else { + XCTFail("error sending to address. Error: \(String(describing: testError))") return } @@ -347,15 +355,18 @@ class AdvancedReOrgTests: XCTestCase { */ let sentTxSyncExpectation = XCTestExpectation(description: "sent tx sync expectation") - try coordinator.sync( - completion: { synchronizer in - let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight - XCTAssertEqual(pMinedHeight, sentTxHeight) - - sentTxSyncExpectation.fulfill() - }, - error: self.handleError - ) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight + XCTAssertEqual(pMinedHeight, sentTxHeight) + continuation.resume() + sentTxSyncExpectation.fulfill() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [sentTxSyncExpectation], timeout: 5) @@ -373,18 +384,22 @@ class AdvancedReOrgTests: XCTestCase { sleep(2) let afterReOrgExpectation = XCTestExpectation(description: "after ReOrg Expectation") - try coordinator.sync( - completion: { synchronizer in - /* - 11. verify that the sent tx is mined and balance is correct - */ - let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight - XCTAssertEqual(pMinedHeight, sentTxHeight) - XCTAssertEqual(initialTotalBalance - sendAmount - Zatoshi(1000), synchronizer.initializer.getBalance()) // fee change on this branch - afterReOrgExpectation.fulfill() - }, - error: self.handleError - ) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + /* + 11. verify that the sent tx is mined and balance is correct + */ + let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight + XCTAssertEqual(pMinedHeight, sentTxHeight) + XCTAssertEqual(initialTotalBalance - sendAmount - Zatoshi(1000), synchronizer.initializer.getBalance()) // fee change on this branch + continuation.resume() + afterReOrgExpectation.fulfill() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [afterReOrgExpectation], timeout: 5) @@ -398,10 +413,16 @@ class AdvancedReOrgTests: XCTestCase { 13. verify that there's no more pending transaction */ let lastSyncExpectation = XCTestExpectation(description: "sync to confirmation") - - try coordinator.sync(completion: { _ in - lastSyncExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + lastSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [lastSyncExpectation], timeout: 5) @@ -670,7 +691,7 @@ class AdvancedReOrgTests: XCTestCase { /// 13. apply height(sentTxHeight + 15) /// 14. sync to latest height /// 15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection - func testReOrgChangesOutboundTxMinedHeight() throws { + func testReOrgChangesOutboundTxMinedHeight() async throws { hookToReOrgNotification() /* @@ -685,9 +706,16 @@ class AdvancedReOrgTests: XCTestCase { /* 1a. sync to latest height */ - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 5) @@ -700,22 +728,18 @@ class AdvancedReOrgTests: XCTestCase { /* 2. send transaction to recipient address */ - coordinator.synchronizer.sendToAddress( - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: Zatoshi(20000), - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: Zatoshi(20000), + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 11) @@ -755,10 +779,17 @@ class AdvancedReOrgTests: XCTestCase { */ let secondSyncExpectation = XCTestExpectation(description: "after send expectation") - try coordinator.sync(completion: { _ in - secondSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + secondSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [secondSyncExpectation], timeout: 5) XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 1) @@ -793,10 +824,17 @@ class AdvancedReOrgTests: XCTestCase { self.expectedReorgHeight = sentTxHeight + 1 let afterReorgExpectation = XCTestExpectation(description: "after reorg sync") - try coordinator.sync(completion: { _ in - afterReorgExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + afterReorgExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [reorgExpectation, afterReorgExpectation], timeout: 5) /* @@ -821,10 +859,17 @@ class AdvancedReOrgTests: XCTestCase { /* 11a. sync to latest height */ - try coordinator.sync(completion: { _ in - yetAnotherExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + yetAnotherExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [yetAnotherExpectation], timeout: 5) /* @@ -851,10 +896,17 @@ class AdvancedReOrgTests: XCTestCase { /* 14. sync to latest height */ - try coordinator.sync(completion: { _ in - thisIsTheLastExpectationIPromess.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + thisIsTheLastExpectationIPromess.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [thisIsTheLastExpectationIPromess], timeout: 5) /* @@ -1023,7 +1075,7 @@ class AdvancedReOrgTests: XCTestCase { /// 7. stage 15 blocks from sentTxHeigth to cause a reorg /// 8. sync to latest height /// 9. verify that there's an expired transaction as a pending transaction - func testReOrgRemovesOutboundTxAndIsNeverMined() throws { + func testReOrgRemovesOutboundTxAndIsNeverMined() async throws { hookToReOrgNotification() /* @@ -1040,10 +1092,17 @@ class AdvancedReOrgTests: XCTestCase { /* 1a. sync to latest height */ - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [firstSyncExpectation], timeout: 5) sleep(1) @@ -1055,22 +1114,18 @@ class AdvancedReOrgTests: XCTestCase { /* 2. send transaction to recipient address */ - coordinator.synchronizer.sendToAddress( - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: Zatoshi(20000), - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try! Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: Zatoshi(20000), + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try! Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 11) @@ -1110,10 +1165,17 @@ class AdvancedReOrgTests: XCTestCase { */ let secondSyncExpectation = XCTestExpectation(description: "after send expectation") - try coordinator.sync(completion: { _ in - secondSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + secondSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [secondSyncExpectation], timeout: 5) let extraBlocks = 25 try coordinator.stageBlockCreate(height: sentTxHeight, count: extraBlocks, nonce: 5) @@ -1123,10 +1185,17 @@ class AdvancedReOrgTests: XCTestCase { sleep(2) let reorgSyncExpectation = XCTestExpectation(description: "reorg sync expectation") - try coordinator.sync(completion: { _ in - reorgSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + reorgSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [reorgExpectation, reorgSyncExpectation], timeout: 5) guard let pendingTx = coordinator.synchronizer.pendingTransactions.first else { @@ -1143,10 +1212,17 @@ class AdvancedReOrgTests: XCTestCase { let lastSyncExpectation = XCTestExpectation(description: "last sync expectation") - try coordinator.sync(completion: { _ in - lastSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + lastSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [lastSyncExpectation], timeout: 5) XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialTotalBalance) diff --git a/Tests/DarksideTests/BalanceTests.swift b/Tests/DarksideTests/BalanceTests.swift index 648b5d5e..e23729bb 100644 --- a/Tests/DarksideTests/BalanceTests.swift +++ b/Tests/DarksideTests/BalanceTests.swift @@ -42,7 +42,7 @@ class BalanceTests: XCTestCase { /** verify that when sending the maximum amount, the transactions are broadcasted properly */ - func testMaxAmountSend() throws { + func testMaxAmountSend() async throws { let notificationHandler = SDKSynchonizerListener() let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation") let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation") @@ -57,9 +57,16 @@ class BalanceTests: XCTestCase { sleep(1) let firstSyncExpectation = XCTestExpectation(description: "first sync expectation") - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 12) // 2 check that there are no unconfirmed funds @@ -79,22 +86,18 @@ class BalanceTests: XCTestCase { } var pendingTx: PendingTransactionEntity? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: maxBalance, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let error): - XCTFail("sendToAddress failed: \(error)") - case .success(let transaction): - pendingTx = transaction - } - self.sentTransactionExpectation.fulfill() - } - ) + do { + let transaction = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: maxBalance, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingTx = transaction + self.sentTransactionExpectation.fulfill() + } catch { + XCTFail("sendToAddress failed: \(error)") + } wait(for: [sentTransactionExpectation], timeout: 20) guard let pendingTx = pendingTx else { @@ -133,23 +136,29 @@ class BalanceTests: XCTestCase { sleep(2) // add enhance breakpoint here let mineExpectation = XCTestExpectation(description: "mineTxExpectation") - try coordinator.sync( - completion: { synchronizer in - let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) - XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now") - XCTAssertTrue(pendingEntity?.isMined ?? false) - XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight) - mineExpectation.fulfill() - }, - error: { error in - guard let error = error else { - XCTFail("unknown error syncing after sending transaction") - return - } - - XCTFail("Error: \(error)") + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync( + completion: { synchronizer in + let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) + XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now") + XCTAssertTrue(pendingEntity?.isMined ?? false) + XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight) + mineExpectation.fulfill() + continuation.resume() + }, error: { error in + guard let error = error else { + XCTFail("unknown error syncing after sending transaction") + return + } + + XCTFail("Error: \(error)") + } + ) + } catch { + continuation.resume(throwing: error) } - ) + } wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5) @@ -166,12 +175,17 @@ class BalanceTests: XCTestCase { notificationHandler.synchronizerMinedTransaction = { transaction in XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)") } - try coordinator.sync(completion: { _ in - confirmExpectation.fulfill() - }, error: { e in - self.handleError(e) - }) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + confirmExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [confirmExpectation], timeout: 5) let confirmedPending = try coordinator.synchronizer.allPendingTransactions() @@ -186,7 +200,7 @@ class BalanceTests: XCTestCase { /** verify that when sending the maximum amount minus one zatoshi, the transactions are broadcasted properly */ - func testMaxAmountMinusOneSend() throws { + func testMaxAmountMinusOneSend() async throws { let notificationHandler = SDKSynchonizerListener() let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation") let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation") @@ -201,9 +215,16 @@ class BalanceTests: XCTestCase { sleep(1) let firstSyncExpectation = XCTestExpectation(description: "first sync expectation") - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 12) // 2 check that there are no unconfirmed funds @@ -221,24 +242,20 @@ class BalanceTests: XCTestCase { XCTFail("failed to create spending keys") return } - + var pendingTx: PendingTransactionEntity? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: maxBalanceMinusOne, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "\(self.description) \(Date().description)"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let error): - XCTFail("sendToAddress failed: \(error)") - case .success(let transaction): - pendingTx = transaction - } - self.sentTransactionExpectation.fulfill() - } - ) + do { + let transaction = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: maxBalanceMinusOne, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "\(self.description) \(Date().description)"), + from: 0) + pendingTx = transaction + self.sentTransactionExpectation.fulfill() + } catch { + XCTFail("sendToAddress failed: \(error)") + } wait(for: [sentTransactionExpectation], timeout: 20) guard let pendingTx = pendingTx else { @@ -277,20 +294,29 @@ class BalanceTests: XCTestCase { sleep(2) // add enhance breakpoint here let mineExpectation = XCTestExpectation(description: "mineTxExpectation") - try coordinator.sync(completion: { synchronizer in - let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) - XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now") - XCTAssertTrue(pendingEntity?.isMined ?? false) - XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight) - mineExpectation.fulfill() - }, error: { error in - guard let e = error else { - XCTFail("unknown error syncing after sending transaction") - return + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync( + completion: { synchronizer in + let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) + XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now") + XCTAssertTrue(pendingEntity?.isMined ?? false) + XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight) + mineExpectation.fulfill() + continuation.resume() + }, error: { error in + guard let error = error else { + XCTFail("unknown error syncing after sending transaction") + return + } + + XCTFail("Error: \(error)") + } + ) + } catch { + continuation.resume(throwing: error) } - - XCTFail("Error: \(e)") - }) + } wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5) @@ -307,11 +333,16 @@ class BalanceTests: XCTestCase { notificationHandler.synchronizerMinedTransaction = { transaction in XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)") } - try coordinator.sync(completion: { _ in - confirmExpectation.fulfill() - }, error: { e in - self.handleError(e) - }) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + confirmExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [confirmExpectation], timeout: 5) @@ -328,7 +359,7 @@ class BalanceTests: XCTestCase { /** verify that when sending the a no change transaction, the transactions are broadcasted properly */ - func testSingleNoteNoChangeTransaction() throws { + func testSingleNoteNoChangeTransaction() async throws { let notificationHandler = SDKSynchonizerListener() let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation") let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation") @@ -343,10 +374,16 @@ class BalanceTests: XCTestCase { sleep(1) let firstSyncExpectation = XCTestExpectation(description: "first sync expectation") - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 12) // 2 check that there are no unconfirmed funds @@ -364,22 +401,18 @@ class BalanceTests: XCTestCase { return } var pendingTx: PendingTransactionEntity? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: maxBalanceMinusOne, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "test send \(self.description) \(Date().description)"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let error): - XCTFail("sendToAddress failed: \(error)") - case .success(let transaction): - pendingTx = transaction - } - self.sentTransactionExpectation.fulfill() - } - ) + do { + let transaction = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: maxBalanceMinusOne, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "test send \(self.description) \(Date().description)"), + from: 0) + pendingTx = transaction + self.sentTransactionExpectation.fulfill() + } catch { + XCTFail("sendToAddress failed: \(error)") + } wait(for: [sentTransactionExpectation], timeout: 20) guard let pendingTx = pendingTx else { @@ -418,20 +451,29 @@ class BalanceTests: XCTestCase { sleep(2) // add enhance breakpoint here let mineExpectation = XCTestExpectation(description: "mineTxExpectation") - try coordinator.sync(completion: { synchronizer in - let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) - XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now") - XCTAssertTrue(pendingEntity?.isMined ?? false) - XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight) - mineExpectation.fulfill() - }, error: { error in - guard let e = error else { - XCTFail("unknown error syncing after sending transaction") - return + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync( + completion: { synchronizer in + let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) + XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now") + XCTAssertTrue(pendingEntity?.isMined ?? false) + XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight) + mineExpectation.fulfill() + continuation.resume() + }, error: { error in + guard let error = error else { + XCTFail("unknown error syncing after sending transaction") + return + } + + XCTFail("Error: \(error)") + } + ) + } catch { + continuation.resume(throwing: error) } - - XCTFail("Error: \(e)") - }) + } wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5) @@ -448,11 +490,16 @@ class BalanceTests: XCTestCase { notificationHandler.synchronizerMinedTransaction = { transaction in XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)") } - try coordinator.sync(completion: { _ in - confirmExpectation.fulfill() - }, error: { e in - self.handleError(e) - }) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + confirmExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [confirmExpectation], timeout: 5) @@ -483,14 +530,21 @@ class BalanceTests: XCTestCase { Error: previous available funds equals to current funds */ // swiftlint:disable cyclomatic_complexity - func testVerifyAvailableBalanceDuringSend() throws { + func testVerifyAvailableBalanceDuringSend() async throws { try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) try coordinator.applyStaged(blockheight: defaultLatestHeight) - try coordinator.sync(completion: { _ in - self.syncedExpectation.fulfill() - }, error: handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + self.syncedExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [syncedExpectation], timeout: 60) @@ -507,27 +561,23 @@ class BalanceTests: XCTestCase { XCTAssertTrue(presendVerifiedBalance >= network.constants.defaultFee(for: defaultLatestHeight) + sendAmount) var pendingTx: PendingTransactionEntity? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: sendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let error): - /* - balance should be the same as before sending if transaction failed - */ - XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), presendVerifiedBalance) - XCTFail("sendToAddress failed: \(error)") - case .success(let transaction): + do { + let transaction = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: sendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingTx = transaction + self.sentTransactionExpectation.fulfill() + } catch { + /* + balance should be the same as before sending if transaction failed + */ + XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), presendVerifiedBalance) + XCTFail("sendToAddress failed: \(error)") - pendingTx = transaction - } - self.sentTransactionExpectation.fulfill() - } - ) + } XCTAssertTrue(coordinator.synchronizer.initializer.getVerifiedBalance() > .zero) wait(for: [sentTransactionExpectation], timeout: 12) @@ -548,16 +598,25 @@ class BalanceTests: XCTestCase { sleep(1) let mineExpectation = XCTestExpectation(description: "mineTxExpectation") - try coordinator.sync(completion: { _ in - mineExpectation.fulfill() - }, error: { error in - guard let e = error else { - XCTFail("unknown error syncing after sending transaction") - return + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync( + completion: { synchronizer in + mineExpectation.fulfill() + continuation.resume() + }, error: { error in + guard let error else { + XCTFail("unknown error syncing after sending transaction") + return + } + + XCTFail("Error: \(error)") + } + ) + } catch { + continuation.resume(throwing: error) } - - XCTFail("Error: \(e)") - }) + } wait(for: [mineExpectation], timeout: 5) @@ -654,15 +713,22 @@ class BalanceTests: XCTestCase { Error: previous total balance funds equals to current total balance */ - func testVerifyTotalBalanceDuringSend() throws { + func testVerifyTotalBalanceDuringSend() async throws { try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) try coordinator.applyStaged(blockheight: defaultLatestHeight) sleep(2) - try coordinator.sync(completion: { _ in - self.syncedExpectation.fulfill() - }, error: handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + self.syncedExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [syncedExpectation], timeout: 5) @@ -677,33 +743,28 @@ class BalanceTests: XCTestCase { XCTAssertTrue(presendBalance >= network.constants.defaultFee(for: defaultLatestHeight) + sendAmount) var pendingTx: PendingTransactionEntity? - var error: Error? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: sendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "test send \(self.description) \(Date().description)"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - // balance should be the same as before sending if transaction failed - error = e - XCTFail("sendToAddress failed: \(e)") - - case .success(let transaction): - pendingTx = transaction - } - self.sentTransactionExpectation.fulfill() - } - ) + var testError: Error? + do { + let transaction = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: sendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "test send \(self.description) \(Date().description)"), + from: 0) + pendingTx = transaction + self.sentTransactionExpectation.fulfill() + } catch { + // balance should be the same as before sending if transaction failed + testError = error + XCTFail("sendToAddress failed: \(error)") + } XCTAssertTrue(coordinator.synchronizer.initializer.getVerifiedBalance() > .zero) wait(for: [sentTransactionExpectation], timeout: 12) - if let e = error { + if let testError { XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), presendBalance) - XCTFail("error: \(e)") + XCTFail("error: \(testError)") return } guard let transaction = pendingTx else { @@ -733,16 +794,25 @@ class BalanceTests: XCTestCase { sleep(2) let mineExpectation = XCTestExpectation(description: "mineTxExpectation") - try coordinator.sync(completion: { _ in - mineExpectation.fulfill() - }, error: { error in - guard let e = error else { - XCTFail("unknown error syncing after sending transaction") - return + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync( + completion: { synchronizer in + mineExpectation.fulfill() + continuation.resume() + }, error: { error in + guard let error else { + XCTFail("unknown error syncing after sending transaction") + return + } + + XCTFail("Error: \(error)") + } + ) + } catch { + continuation.resume(throwing: error) } - - XCTFail("Error: \(e)") - }) + } wait(for: [mineExpectation], timeout: 5) @@ -802,7 +872,7 @@ class BalanceTests: XCTestCase { There’s a change note of value (previous note value - sent amount) */ - func testVerifyChangeTransaction() throws { + func testVerifyChangeTransaction() async throws { try FakeChainBuilder.buildSingleNoteChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) try coordinator.applyStaged(blockheight: defaultLatestHeight) @@ -814,9 +884,16 @@ class BalanceTests: XCTestCase { /* sync to current tip */ - try coordinator.sync(completion: { _ in - self.syncedExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + self.syncedExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [syncedExpectation], timeout: 6) @@ -833,26 +910,18 @@ class BalanceTests: XCTestCase { */ let memo = try Memo(string: "shielding is fun!") var pendingTx: PendingTransactionEntity? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKeys, - zatoshi: sendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: memo, - from: 0, - resultBlock: { sendResult in - DispatchQueue.main.async { - switch sendResult { - case .failure(let sendError): - XCTFail("error sending \(sendError)") - - case .success(let transaction): - pendingTx = transaction - } - - sendExpectation.fulfill() - } - } - ) + do { + let transaction = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKeys, + zatoshi: sendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: memo, + from: 0) + pendingTx = transaction + sendExpectation.fulfill() + } catch { + XCTFail("error sending \(error)") + } wait(for: [createToAddressExpectation], timeout: 30) let syncToMinedheightExpectation = XCTestExpectation(description: "sync to mined height + 1") @@ -875,94 +944,94 @@ class BalanceTests: XCTestCase { /* Sync to that block */ - try coordinator.sync( - completion: { synchronizer in - let confirmedTx: ConfirmedTransactionEntity! - do { - confirmedTx = try synchronizer.allClearedTransactions().first(where: { confirmed -> Bool in - confirmed.transactionEntity.transactionId == pendingTx?.transactionEntity.transactionId - }) - } catch { - XCTFail("Error retrieving cleared transactions") - return - } + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + let confirmedTx: ConfirmedTransactionEntity! + do { + confirmedTx = try synchronizer.allClearedTransactions().first(where: { confirmed -> Bool in + confirmed.transactionEntity.transactionId == pendingTx?.transactionEntity.transactionId + }) + } catch { + XCTFail("Error retrieving cleared transactions") + return + } - /* - There’s a sent transaction matching the amount sent to the given zAddr - */ - XCTAssertEqual(confirmedTx.value, self.sendAmount) - XCTAssertEqual(confirmedTx.toAddress, self.testRecipientAddress) - do { + /* + There’s a sent transaction matching the amount sent to the given zAddr + */ + XCTAssertEqual(confirmedTx.value, self.sendAmount) + XCTAssertEqual(confirmedTx.toAddress, self.testRecipientAddress) let confirmedMemo = try confirmedTx.memo.intoMemoBytes().intoMemo() XCTAssertEqual(confirmedMemo, memo) - } catch { - XCTFail("failed retrieving memo from confirmed transaction. Error: \(error.localizedDescription)") - } - guard let transactionId = confirmedTx.rawTransactionId else { - XCTFail("no raw transaction id") - return - } + guard let transactionId = confirmedTx.rawTransactionId else { + XCTFail("no raw transaction id") + return + } - /* - Find out what note was used - */ - let sentNotesRepo = SentNotesSQLDAO( - dbProvider: SimpleConnectionProvider( - path: synchronizer.initializer.dataDbURL.absoluteString, - readonly: true + /* + Find out what note was used + */ + let sentNotesRepo = SentNotesSQLDAO( + dbProvider: SimpleConnectionProvider( + path: synchronizer.initializer.dataDbURL.absoluteString, + readonly: true + ) ) - ) - guard let sentNote = try? sentNotesRepo.sentNote(byRawTransactionId: transactionId) else { - XCTFail("Could not finde sent note with transaction Id \(transactionId)") - return - } + guard let sentNote = try? sentNotesRepo.sentNote(byRawTransactionId: transactionId) else { + XCTFail("Could not finde sent note with transaction Id \(transactionId)") + return + } - let receivedNotesRepo = ReceivedNotesSQLDAO( - dbProvider: SimpleConnectionProvider( - path: self.coordinator.synchronizer.initializer.dataDbURL.absoluteString, - readonly: true + let receivedNotesRepo = ReceivedNotesSQLDAO( + dbProvider: SimpleConnectionProvider( + path: self.coordinator.synchronizer.initializer.dataDbURL.absoluteString, + readonly: true + ) ) - ) - /* - get change note - */ - guard let receivedNote = try? receivedNotesRepo.receivedNote(byRawTransactionId: transactionId) else { - XCTFail("Could not find received not with change for transaction Id \(transactionId)") - return - } + /* + get change note + */ + guard let receivedNote = try? receivedNotesRepo.receivedNote(byRawTransactionId: transactionId) else { + XCTFail("Could not find received not with change for transaction Id \(transactionId)") + return + } - /* - There’s a change note of value (previous note value - sent amount) - */ - XCTAssertEqual( - previousVerifiedBalance - self.sendAmount - self.network.constants.defaultFee(for: self.defaultLatestHeight), - Zatoshi(Int64(receivedNote.value)) - ) + /* + There’s a change note of value (previous note value - sent amount) + */ + XCTAssertEqual( + previousVerifiedBalance - self.sendAmount - self.network.constants.defaultFee(for: self.defaultLatestHeight), + Zatoshi(Int64(receivedNote.value)) + ) - /* - Balance meets verified Balance and total balance criteria - */ - self.verifiedBalanceValidation( - previousBalance: previousVerifiedBalance, - spentNoteValue: Zatoshi(Int64(sentNote.value)), - changeValue: Zatoshi(Int64(receivedNote.value)), - sentAmount: self.sendAmount, - currentVerifiedBalance: synchronizer.initializer.getVerifiedBalance() - ) + /* + Balance meets verified Balance and total balance criteria + */ + self.verifiedBalanceValidation( + previousBalance: previousVerifiedBalance, + spentNoteValue: Zatoshi(Int64(sentNote.value)), + changeValue: Zatoshi(Int64(receivedNote.value)), + sentAmount: self.sendAmount, + currentVerifiedBalance: synchronizer.initializer.getVerifiedBalance() + ) - self.totalBalanceValidation( - totalBalance: synchronizer.initializer.getBalance(), - previousTotalbalance: previousTotalBalance, - sentAmount: self.sendAmount - ) + self.totalBalanceValidation( + totalBalance: synchronizer.initializer.getBalance(), + previousTotalbalance: previousTotalBalance, + sentAmount: self.sendAmount + ) - syncToMinedheightExpectation.fulfill() - }, - error: self.handleError - ) + syncToMinedheightExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [syncToMinedheightExpectation], timeout: 5) } @@ -990,15 +1059,21 @@ class BalanceTests: XCTestCase { Verified Balance is equal to verified balance previously shown before sending the expired transaction */ - func testVerifyBalanceAfterExpiredTransaction() throws { + func testVerifyBalanceAfterExpiredTransaction() async throws { try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) try coordinator.applyStaged(blockheight: self.defaultLatestHeight) sleep(2) - try coordinator.sync(completion: { _ in - self.syncedExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + self.syncedExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [syncedExpectation], timeout: 5) guard let spendingKey = coordinator.spendingKeys?.first else { @@ -1010,24 +1085,20 @@ class BalanceTests: XCTestCase { let previousTotalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance() let sendExpectation = XCTestExpectation(description: "send expectation") var pendingTx: PendingTransactionEntity? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: sendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "test send \(self.description)"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let error): - // balance should be the same as before sending if transaction failed - XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), previousVerifiedBalance) - XCTAssertEqual(self.coordinator.synchronizer.initializer.getBalance(), previousTotalBalance) - XCTFail("sendToAddress failed: \(error)") - case .success(let pending): - pendingTx = pending - } - } - ) + do { + let pending = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: sendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "test send \(self.description)"), + from: 0) + pendingTx = pending + } catch { + // balance should be the same as before sending if transaction failed + XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), previousVerifiedBalance) + XCTAssertEqual(self.coordinator.synchronizer.initializer.getBalance(), previousTotalBalance) + XCTFail("sendToAddress failed: \(error)") + } wait(for: [sendExpectation], timeout: 12) @@ -1043,10 +1114,16 @@ class BalanceTests: XCTestCase { try coordinator.applyStaged(blockheight: expiryHeight + 1) sleep(2) - try coordinator.sync(completion: { _ in - expirationSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + expirationSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [expirationSyncExpectation], timeout: 5) /* diff --git a/Tests/DarksideTests/NetworkUpgradeTests.swift b/Tests/DarksideTests/NetworkUpgradeTests.swift index fc72f8c3..aca7d67c 100644 --- a/Tests/DarksideTests/NetworkUpgradeTests.swift +++ b/Tests/DarksideTests/NetworkUpgradeTests.swift @@ -43,7 +43,7 @@ class NetworkUpgradeTests: XCTestCase { /** Given that a wallet had funds prior to activation it can spend them after activation */ - func testSpendPriorFundsAfterActivation() throws { + func testSpendPriorFundsAfterActivation() async throws { try FakeChainBuilder.buildChain( darksideWallet: coordinator.service, birthday: birthday, @@ -58,10 +58,16 @@ class NetworkUpgradeTests: XCTestCase { try coordinator.applyStaged(blockheight: activationHeight - ZcashSDK.defaultStaleTolerance) sleep(5) - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 120) let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance() guard verifiedBalance > network.constants.defaultFee(for: activationHeight) else { @@ -79,22 +85,18 @@ class NetworkUpgradeTests: XCTestCase { /* send transaction to recipient address */ - coordinator.synchronizer.sendToAddress( - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: spendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: spendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 11) @@ -123,10 +125,17 @@ class NetworkUpgradeTests: XCTestCase { sleep(1) let afterSendExpectation = XCTestExpectation(description: "aftersend") - try coordinator.sync(completion: { _ in - afterSendExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + afterSendExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [afterSendExpectation], timeout: 10) XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), verifiedBalance - spendAmount) @@ -135,7 +144,7 @@ class NetworkUpgradeTests: XCTestCase { /** Given that a wallet receives funds after activation it can spend them when confirmed */ - func testSpendPostActivationFundsAfterConfirmation() throws { + func testSpendPostActivationFundsAfterConfirmation() async throws { try FakeChainBuilder.buildChainPostActivationFunds( darksideWallet: coordinator.service, birthday: birthday, @@ -148,9 +157,16 @@ class NetworkUpgradeTests: XCTestCase { try coordinator.applyStaged(blockheight: activationHeight + 10) sleep(3) - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 120) guard try !coordinator.synchronizer.allReceivedTransactions().filter({ $0.minedHeight > activationHeight }).isEmpty else { @@ -168,22 +184,18 @@ class NetworkUpgradeTests: XCTestCase { /* send transaction to recipient address */ - coordinator.synchronizer.sendToAddress( - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: spendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: spendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 11) @@ -197,9 +209,16 @@ class NetworkUpgradeTests: XCTestCase { let afterSendExpectation = XCTestExpectation(description: "aftersend") - try coordinator.sync(completion: { _ in - afterSendExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + afterSendExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [afterSendExpectation], timeout: 10) } @@ -207,7 +226,7 @@ class NetworkUpgradeTests: XCTestCase { /** Given that a wallet sends funds some between (activation - expiry_height) and activation, those funds are shown as sent if mined. */ - func testSpendMinedSpendThatExpiresOnActivation() throws { + func testSpendMinedSpendThatExpiresOnActivation() async throws { try FakeChainBuilder.buildChain( darksideWallet: coordinator.service, birthday: birthday, @@ -222,9 +241,16 @@ class NetworkUpgradeTests: XCTestCase { try coordinator.applyStaged(blockheight: activationHeight - 10) sleep(3) - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 120) let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance() @@ -237,22 +263,18 @@ class NetworkUpgradeTests: XCTestCase { /* send transaction to recipient address */ - coordinator.synchronizer.sendToAddress( - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: spendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: spendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 11) @@ -283,9 +305,16 @@ class NetworkUpgradeTests: XCTestCase { let afterSendExpectation = XCTestExpectation(description: "aftersend") - try coordinator.sync(completion: { _ in - afterSendExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + afterSendExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [afterSendExpectation], timeout: 10) @@ -303,7 +332,7 @@ class NetworkUpgradeTests: XCTestCase { /** Given that a wallet sends funds somewhere between (activation - expiry_height) and activation, those funds are available if expired after expiration height. */ - func testExpiredSpendAfterActivation() throws { + func testExpiredSpendAfterActivation() async throws { try FakeChainBuilder.buildChain( darksideWallet: coordinator.service, birthday: birthday, @@ -320,9 +349,16 @@ class NetworkUpgradeTests: XCTestCase { let verifiedBalancePreActivation: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance() - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 120) let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance() @@ -338,22 +374,18 @@ class NetworkUpgradeTests: XCTestCase { /* send transaction to recipient address */ - coordinator.synchronizer.sendToAddress( - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: spendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: spendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 11) @@ -380,9 +412,16 @@ class NetworkUpgradeTests: XCTestCase { let afterSendExpectation = XCTestExpectation(description: "aftersend") - try coordinator.sync(completion: { _ in - afterSendExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + afterSendExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [afterSendExpectation], timeout: 10) @@ -400,7 +439,7 @@ class NetworkUpgradeTests: XCTestCase { /** Given that a wallet has notes both received prior and after activation these can be combined to supply a larger amount spend. */ - func testCombinePreActivationNotesAndPostActivationNotesOnSpend() throws { + func testCombinePreActivationNotesAndPostActivationNotesOnSpend() async throws { try FakeChainBuilder.buildChainMixedFunds( darksideWallet: coordinator.service, birthday: birthday, @@ -415,9 +454,16 @@ class NetworkUpgradeTests: XCTestCase { try coordinator.applyStaged(blockheight: activationHeight - 1) sleep(3) - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 120) @@ -427,10 +473,17 @@ class NetworkUpgradeTests: XCTestCase { sleep(2) let secondSyncExpectation = XCTestExpectation(description: "second sync") - try coordinator.sync(completion: { _ in - secondSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + secondSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [secondSyncExpectation], timeout: 10) guard try !coordinator.synchronizer.allReceivedTransactions().filter({ $0.minedHeight > activationHeight }).isEmpty else { XCTFail("this test requires funds received after activation height") @@ -450,22 +503,18 @@ class NetworkUpgradeTests: XCTestCase { /* send transaction to recipient address */ - coordinator.synchronizer.sendToAddress( - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: spendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: spendAmount, + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 15) diff --git a/Tests/DarksideTests/PendingTransactionUpdatesTest.swift b/Tests/DarksideTests/PendingTransactionUpdatesTest.swift index ff5b9490..6178dd53 100644 --- a/Tests/DarksideTests/PendingTransactionUpdatesTest.swift +++ b/Tests/DarksideTests/PendingTransactionUpdatesTest.swift @@ -61,7 +61,7 @@ class PendingTransactionUpdatesTest: XCTestCase { reorgExpectation.fulfill() } - func testPendingTransactionMinedHeightUpdated() throws { + func testPendingTransactionMinedHeightUpdated() async throws { /* 1. create fake chain */ @@ -78,10 +78,16 @@ class PendingTransactionUpdatesTest: XCTestCase { 1a. sync to latest height */ LoggerProxy.info("1a. sync to latest height") - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 5) sleep(1) @@ -93,23 +99,19 @@ class PendingTransactionUpdatesTest: XCTestCase { 2. send transaction to recipient address */ LoggerProxy.info("2. send transaction to recipient address") - coordinator.synchronizer.sendToAddress( - // swiftlint:disable:next force_unwrapping - spendingKey: self.coordinator.spendingKeys!.first!, - zatoshi: Zatoshi(20000), - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "this is a test"), - from: 0, - resultBlock: { result in - switch result { - case .failure(let e): - self.handleError(e) - case .success(let pendingTx): - pendingEntity = pendingTx - } - sendExpectation.fulfill() - } - ) + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + // swiftlint:disable:next force_unwrapping + spendingKey: self.coordinator.spendingKeys!.first!, + zatoshi: Zatoshi(20000), + toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "this is a test"), + from: 0) + pendingEntity = pendingTx + sendExpectation.fulfill() + } catch { + self.handleError(error) + } wait(for: [sendExpectation], timeout: 11) @@ -167,13 +169,17 @@ class PendingTransactionUpdatesTest: XCTestCase { LoggerProxy.info("6. sync to latest height") let secondSyncExpectation = XCTestExpectation(description: "after send expectation") - try coordinator.sync( - completion: { _ in - secondSyncExpectation.fulfill() - }, - error: self.handleError - ) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + secondSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [secondSyncExpectation], timeout: 5) XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 1) @@ -207,10 +213,17 @@ class PendingTransactionUpdatesTest: XCTestCase { */ LoggerProxy.info("last sync to latest height: \(lastStageHeight)") - try coordinator.sync(completion: { _ in - syncToConfirmExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + syncToConfirmExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [syncToConfirmExpectation], timeout: 6) var supposedlyPendingUnexistingTransaction: PendingTransactionEntity? diff --git a/Tests/DarksideTests/RewindRescanTests.swift b/Tests/DarksideTests/RewindRescanTests.swift index 163965cb..f5790c13 100644 --- a/Tests/DarksideTests/RewindRescanTests.swift +++ b/Tests/DarksideTests/RewindRescanTests.swift @@ -9,14 +9,14 @@ import XCTest @testable import TestUtils @testable import ZcashLightClientKit -// swiftlint:disable type_body_length implicitly_unwrapped_optional +// swiftlint:disable type_body_length implicitly_unwrapped_optional force_try class RewindRescanTests: XCTestCase { // TODO: Parameterize this from environment? // swiftlint:disable:next line_length let seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" // TODO: Parameterize this from environment - let testRecipientAddress = "zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a" + let testRecipientAddress = try! Recipient("zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a", network: .mainnet) let sendAmount: Int64 = 1000 let defaultLatestHeight: BlockHeight = 663175 let branchID = "2bb40e60" @@ -107,7 +107,7 @@ class RewindRescanTests: XCTestCase { XCTAssertEqual(totalBalance, coordinator.synchronizer.initializer.getBalance()) } - func testRescanToHeight() throws { + func testRescanToHeight() async throws { // 1 sync and get spendable funds try FakeChainBuilder.buildChainWithTxsFarFromEachOther( darksideWallet: coordinator.service, @@ -121,13 +121,16 @@ class RewindRescanTests: XCTestCase { let initialVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance() let firstSyncExpectation = XCTestExpectation(description: "first sync expectation") - try coordinator.sync( - completion: { _ in - firstSyncExpectation.fulfill() - }, - error: handleError - ) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 20) let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance() let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance() @@ -154,10 +157,17 @@ class RewindRescanTests: XCTestCase { let secondScanExpectation = XCTestExpectation(description: "rescan") - try coordinator.sync(completion: { _ in - secondScanExpectation.fulfill() - }, error: handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + secondScanExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [secondScanExpectation], timeout: 20) // verify that the balance still adds up @@ -166,20 +176,16 @@ class RewindRescanTests: XCTestCase { // try to spend the funds let sendExpectation = XCTestExpectation(description: "after rewind expectation") - coordinator.synchronizer.sendToAddress( - spendingKey: coordinator.spendingKey, - zatoshi: Zatoshi(1000), - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: .empty, - from: 0 - ) { result in - sendExpectation.fulfill() - switch result { - case .success(let pendingTx): - XCTAssertEqual(Zatoshi(1000), pendingTx.value) - case .failure(let error): - XCTFail("sending fail: \(error)") - } + do { + let pendingTx = try await coordinator.synchronizer.sendToAddress( + spendingKey: coordinator.spendingKey, + zatoshi: Zatoshi(1000), + toAddress: testRecipientAddress, + memo: .empty, + from: 0) + XCTAssertEqual(Zatoshi(1000), pendingTx.value) + } catch { + XCTFail("sending fail: \(error)") } wait(for: [sendExpectation], timeout: 15) } @@ -231,7 +237,7 @@ class RewindRescanTests: XCTestCase { XCTAssertEqual(totalBalance, coordinator.synchronizer.initializer.getBalance()) } - func testRewindAfterSendingTransaction() throws { + func testRewindAfterSendingTransaction() async throws { let notificationHandler = SDKSynchonizerListener() let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation") let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation") @@ -246,10 +252,16 @@ class RewindRescanTests: XCTestCase { sleep(1) let firstSyncExpectation = XCTestExpectation(description: "first sync expectation") - try coordinator.sync(completion: { _ in - firstSyncExpectation.fulfill() - }, error: handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + firstSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [firstSyncExpectation], timeout: 12) // 2 check that there are no unconfirmed funds @@ -267,20 +279,17 @@ class RewindRescanTests: XCTestCase { return } var pendingTx: PendingTransactionEntity? - coordinator.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: maxBalance, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "test send \(self.description) \(Date().description)"), - from: 0 - ) { result in - switch result { - case .failure(let error): - XCTFail("sendToAddress failed: \(error)") - case .success(let transaction): - pendingTx = transaction - } + do { + let transaction = try await coordinator.synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: maxBalance, + toAddress: testRecipientAddress, + memo: try Memo(string: "test send \(self.description) \(Date().description)"), + from: 0) + pendingTx = transaction self.sentTransactionExpectation.fulfill() + } catch { + XCTFail("sendToAddress failed: \(error)") } wait(for: [sentTransactionExpectation], timeout: 20) guard let pendingTx = pendingTx else { @@ -320,25 +329,30 @@ class RewindRescanTests: XCTestCase { let mineExpectation = XCTestExpectation(description: "mineTxExpectation") - try coordinator.sync( - completion: { synchronizer in - let pendingTransaction = synchronizer.pendingTransactions - .first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) - XCTAssertNotNil(pendingTransaction, "pending transaction should have been mined by now") - XCTAssertTrue(pendingTransaction?.isMined ?? false) - XCTAssertEqual(pendingTransaction?.minedHeight, sentTxHeight) - mineExpectation.fulfill() - }, - error: { error in - guard let e = error else { - XCTFail("unknown error syncing after sending transaction") - return - } - - XCTFail("Error: \(e)") + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync( + completion: { synchronizer in + let pendingTransaction = synchronizer.pendingTransactions + .first(where: { $0.rawTransactionId == pendingTx.rawTransactionId }) + XCTAssertNotNil(pendingTransaction, "pending transaction should have been mined by now") + XCTAssertTrue(pendingTransaction?.isMined ?? false) + XCTAssertEqual(pendingTransaction?.minedHeight, sentTxHeight) + mineExpectation.fulfill() + continuation.resume() + }, error: { error in + guard let error else { + XCTFail("unknown error syncing after sending transaction") + return + } + + XCTFail("Error: \(error)") + } + ) + } catch { + continuation.resume(throwing: error) } - ) - + } wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5) // 7 advance to confirmation @@ -375,15 +389,16 @@ class RewindRescanTests: XCTestCase { XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)") } - try coordinator.sync( - completion: { _ in - confirmExpectation.fulfill() - }, - error: { e in - self.handleError(e) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + confirmExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) } - ) - + } wait(for: [confirmExpectation], timeout: 10) let confirmedPending = try coordinator.synchronizer.allPendingTransactions() diff --git a/Tests/DarksideTests/ShieldFundsTests.swift b/Tests/DarksideTests/ShieldFundsTests.swift index 342ff2d5..daad976d 100644 --- a/Tests/DarksideTests/ShieldFundsTests.swift +++ b/Tests/DarksideTests/ShieldFundsTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import TestUtils @testable import ZcashLightClientKit + class ShieldFundsTests: XCTestCase { // TODO: Parameterize this from environment? // swiftlint:disable:next line_length @@ -82,7 +83,7 @@ class ShieldFundsTests: XCTestCase { /// 15. sync up to the new chain tip /// verify that the shielded transactions are confirmed /// - func testShieldFunds() throws { + func testShieldFunds() async throws { // 1. load the dataset try coordinator.service.useDataset(from: "https://github.com/zcash-hackworks/darksidewalletd-test-data/blob/master/shield-funds/1631000.txt") @@ -110,15 +111,19 @@ class ShieldFundsTests: XCTestCase { let preTxExpectation = XCTestExpectation(description: "pre receive") // 3. sync up to that height - try coordinator.sync( - completion: { synchro in - initialVerifiedBalance = synchro.initializer.getVerifiedBalance() - initialTotalBalance = synchro.initializer.getBalance() - preTxExpectation.fulfill() - shouldContinue = true - }, - error: self.handleError - ) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + initialVerifiedBalance = synchronizer.initializer.getVerifiedBalance() + initialTotalBalance = synchronizer.initializer.getBalance() + preTxExpectation.fulfill() + shouldContinue = true + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [preTxExpectation], timeout: 10) @@ -149,14 +154,17 @@ class ShieldFundsTests: XCTestCase { shouldContinue = false // 6. Sync and find the UXTO on chain. - try coordinator.sync( - completion: { synchro in - tFundsDetectionExpectation.fulfill() - shouldContinue = true - }, - error: self.handleError - ) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + shouldContinue = true + tFundsDetectionExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [tFundsDetectionExpectation], timeout: 2) // at this point the balance should be zero for shielded, then zero verified transparent funds @@ -176,13 +184,17 @@ class ShieldFundsTests: XCTestCase { sleep(2) // 8. sync up to chain tip. - try coordinator.sync( - completion: { synchro in - tFundsConfirmationSyncExpectation.fulfill() - shouldContinue = true - }, - error: self.handleError - ) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + shouldContinue = true + tFundsConfirmationSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [tFundsConfirmationSyncExpectation], timeout: 5) @@ -208,21 +220,17 @@ class ShieldFundsTests: XCTestCase { var shieldingPendingTx: PendingTransactionEntity? // shield the funds - coordinator.synchronizer.shieldFunds( - transparentAccountPrivateKey: transparentAccountPrivateKey, - memo: try Memo(string: "shield funds"), - from: 0 - ) { result in - switch result { - case .failure(let error): - XCTFail("Failed With error: \(error.localizedDescription)") - - case .success(let pendingTx): - shouldContinue = true - XCTAssertEqual(pendingTx.value, Zatoshi(10000)) - shieldingPendingTx = pendingTx - } + do { + let pendingTx = try await coordinator.synchronizer.shieldFunds( + transparentAccountPrivateKey: transparentAccountPrivateKey, + memo: try Memo(string: "shield funds"), + from: 0) + shouldContinue = true + XCTAssertEqual(pendingTx.value, Zatoshi(10000)) + shieldingPendingTx = pendingTx shieldFundsExpectation.fulfill() + } catch { + XCTFail("Failed With error: \(error.localizedDescription)") } wait(for: [shieldFundsExpectation], timeout: 30) @@ -262,14 +270,17 @@ class ShieldFundsTests: XCTestCase { // 13. sync up to chain tip let postShieldSyncExpectation = XCTestExpectation(description: "sync Post shield") shouldContinue = false - try coordinator.sync( - completion: { synchro in - postShieldSyncExpectation.fulfill() - shouldContinue = true - }, - error: self.handleError - ) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + shouldContinue = true + postShieldSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [postShieldSyncExpectation], timeout: 3) guard shouldContinue else { return } @@ -292,13 +303,17 @@ class ShieldFundsTests: XCTestCase { shouldContinue = false // 15. sync up to the new chain tip - try coordinator.sync( - completion: { synchro in - confirmationExpectation.fulfill() - shouldContinue = true - }, - error: self.handleError - ) + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + shouldContinue = true + confirmationExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [confirmationExpectation], timeout: 5) diff --git a/Tests/DarksideTests/SynchronizerTests.swift b/Tests/DarksideTests/SynchronizerTests.swift new file mode 100644 index 00000000..d12c3fc1 --- /dev/null +++ b/Tests/DarksideTests/SynchronizerTests.swift @@ -0,0 +1,128 @@ +// +// SynchronizerTests.swift +// DarksideTests +// +// Created by Francisco Gindre on 9/16/22. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +// swiftlint:disable implicitly_unwrapped_optional force_unwrapping type_body_length +final class SynchronizerTests: XCTestCase { + + // TODO: Parameterize this from environment? + // swiftlint:disable:next line_length + var seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" + + // TODO: Parameterize this from environment + let testRecipientAddress = "zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a" + + let sendAmount = Zatoshi(1000) + var birthday: BlockHeight = 663150 + let defaultLatestHeight: BlockHeight = 663175 + var coordinator: TestCoordinator! + var syncedExpectation = XCTestExpectation(description: "synced") + var sentTransactionExpectation = XCTestExpectation(description: "sent") + var expectedReorgHeight: BlockHeight = 665188 + var expectedRewindHeight: BlockHeight = 665188 + var reorgExpectation = XCTestExpectation(description: "reorg") + let branchID = "2bb40e60" + let chainName = "main" + let network = DarksideWalletDNetwork() + + override func setUpWithError() throws { + try super.setUpWithError() + coordinator = try TestCoordinator( + seed: seedPhrase, + walletBirthday: birthday + 50, //don't use an exact birthday, users never do. + channelProvider: ChannelProvider(), + network: network + ) + try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + NotificationCenter.default.removeObserver(self) + try coordinator.stop() + try? FileManager.default.removeItem(at: coordinator.databases.cacheDB) + try? FileManager.default.removeItem(at: coordinator.databases.dataDB) + try? FileManager.default.removeItem(at: coordinator.databases.pendingDB) + } + + @objc func handleReorg(_ notification: Notification) { + guard + let reorgHeight = notification.userInfo?[CompactBlockProcessorNotificationKey.reorgHeight] as? BlockHeight, + let rewindHeight = notification.userInfo?[CompactBlockProcessorNotificationKey.rewindHeight] as? BlockHeight + else { + XCTFail("empty reorg notification") + return + } + + logger!.debug("--- REORG DETECTED \(reorgHeight)--- RewindHeight: \(rewindHeight)", file: #file, function: #function, line: #line) + + XCTAssertEqual(reorgHeight, expectedReorgHeight) + reorgExpectation.fulfill() + } + + func testSynchronizerStops() throws { + hookToReOrgNotification() + + /* + 1. create fake chain + */ + let fullSyncLength = 100_000 + + try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength) + + try coordinator.applyStaged(blockheight: birthday + fullSyncLength) + + sleep(10) + + let syncStoppedExpectation = XCTestExpectation(description: "SynchronizerStopped Expectation") + syncStoppedExpectation.subscribe(to: .synchronizerStopped, object: nil) + + let processorStoppedExpectation = XCTestExpectation(description: "ProcessorStopped Expectation") + processorStoppedExpectation.subscribe(to: .blockProcessorStopped, object: nil) + + /* + sync to latest height + */ + try coordinator.sync(completion: { _ in + XCTFail("Sync should have stopped") + }, error: { error in + _ = try? self.coordinator.stop() + + guard let testError = error else { + XCTFail("failed with nil error") + return + } + XCTFail("Failed with error: \(testError)") + }) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + self.coordinator.synchronizer.stop() + } + + wait(for: [processorStoppedExpectation,syncStoppedExpectation], timeout: 6, enforceOrder: true) + + XCTAssertEqual(coordinator.synchronizer.status, .stopped) + XCTAssertEqual(coordinator.synchronizer.blockProcessor.state.getState(), .stopped) + } + + func handleError(_ error: Error?) { + _ = try? coordinator.stop() + guard let testError = error else { + XCTFail("failed with nil error") + return + } + XCTFail("Failed with error: \(testError)") + } + + func hookToReOrgNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(handleReorg(_:)), name: .blockProcessorHandledReOrg, object: nil) + + } +} diff --git a/Tests/DarksideTests/Z2TReceiveTests.swift b/Tests/DarksideTests/Z2TReceiveTests.swift index 3dcfdf82..eea3a06b 100644 --- a/Tests/DarksideTests/Z2TReceiveTests.swift +++ b/Tests/DarksideTests/Z2TReceiveTests.swift @@ -69,7 +69,7 @@ class Z2TReceiveTests: XCTestCase { self.foundTransactionsExpectation.fulfill() } - func testFoundTransactions() throws { + func testFoundTransactions() async throws { subscribeToFoundTransactions() try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName) let receivedTxHeight: BlockHeight = 663188 @@ -85,42 +85,42 @@ class Z2TReceiveTests: XCTestCase { /* 3. sync up to received_Tx_height */ - try coordinator.sync( - completion: { _ in - preTxExpectation.fulfill() - }, - error: self.handleError - ) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + preTxExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } wait(for: [preTxExpectation, foundTransactionsExpectation], timeout: 5) let sendExpectation = XCTestExpectation(description: "sendToAddress") var pendingEntity: PendingTransactionEntity? - var error: Error? + var testError: Error? let sendAmount = Zatoshi(10000) /* 4. create transaction */ - coordinator.synchronizer.sendToAddress( - spendingKey: coordinator.spendingKeys!.first!, - zatoshi: sendAmount, - toAddress: try! Recipient(testRecipientAddress, network: self.network.networkType), - memo: try Memo(string: "test transaction"), - from: 0 - ) { result in - switch result { - case .success(let pending): - pendingEntity = pending - case .failure(let e): - error = e - } + do { + let pending = try await coordinator.synchronizer.sendToAddress( + spendingKey: coordinator.spendingKeys!.first!, + zatoshi: sendAmount, + toAddress: try! Recipient(testRecipientAddress, network: self.network.networkType), + memo: try Memo(string: "test transaction"), + from: 0) + pendingEntity = pending sendExpectation.fulfill() + } catch { + testError = error } wait(for: [sendExpectation], timeout: 12) guard pendingEntity != nil else { - XCTFail("error sending to address. Error: \(String(describing: error))") + XCTFail("error sending to address. Error: \(String(describing: testError))") return } @@ -154,13 +154,20 @@ class Z2TReceiveTests: XCTestCase { */ let sentTxSyncExpectation = XCTestExpectation(description: "sent tx sync expectation") - try coordinator.sync(completion: { synchronizer in - let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight - XCTAssertEqual(pMinedHeight, sentTxHeight) - - sentTxSyncExpectation.fulfill() - }, error: self.handleError) - + try await withCheckedThrowingContinuation { continuation in + do { + try coordinator.sync(completion: { synchronizer in + let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight + XCTAssertEqual(pMinedHeight, sentTxHeight) + + sentTxSyncExpectation.fulfill() + continuation.resume() + }, error: self.handleError) + } catch { + continuation.resume(throwing: error) + } + } + wait(for: [sentTxSyncExpectation, foundTransactionsExpectation], timeout: 5) } diff --git a/Tests/OfflineTests/DerivationToolTests/DerivationToolTestnetTests.swift b/Tests/OfflineTests/DerivationToolTests/DerivationToolTestnetTests.swift index d74edbfb..e2880fda 100644 --- a/Tests/OfflineTests/DerivationToolTests/DerivationToolTestnetTests.swift +++ b/Tests/OfflineTests/DerivationToolTests/DerivationToolTestnetTests.swift @@ -12,7 +12,7 @@ import XCTest class DerivationToolTestnetTests: XCTestCase { var seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" //TODO: Parameterize this from environment? var seedData: Data = Data(base64Encoded: "9VDVOZZZOWWHpZtq1Ebridp3Qeux5C+HwiRR0g7Oi7HgnMs8Gfln83+/Q1NnvClcaSwM4ADFL1uZHxypEWlWXg==")! - let testRecipientAddress = UnifiedAddress(validatedEncoding: "utest1uqmec4a2njqz2z2rwppchsd06qe7a0jh4jmsqr0yy99m9er9646zlxunf3v8qr0hncgv86e8a62vxy0qa32qzetmj8s57yudmyx9zav6f52nurclsqjkqtjtpz6vg679p6wkczpl2wu", network: .mainnet) //TODO: Parameterize this from environment + let testRecipientAddress = UnifiedAddress(validatedEncoding: "utest1uqmec4a2njqz2z2rwppchsd06qe7a0jh4jmsqr0yy99m9er9646zlxunf3v8qr0hncgv86e8a62vxy0qa32qzetmj8s57yudmyx9zav6f52nurclsqjkqtjtpz6vg679p6wkczpl2wu", network: .testnet) //TODO: Parameterize this from environment let expectedSpendingKey = SaplingExtendedSpendingKey(validatedEncoding: "secret-extended-key-test1qdxykmuaqqqqpqqg3x5c02p4rhw0rtszr8ln4xl7g6wg6qzsqgn445qsu3cq4vd6lk8xce3d4jw7s8ln5yjp6fqv2g0nzue2hc0kv5t004vklvlenncscq9flwh5vf5qnv0hnync72n7gjn70u47765v3kyrxytx50g730svvmhhlazn5rj8mshh470fkrmzg4xarhrqlygg8f486307ujhndwhsw2h7ddzf89k3534aeu0ypz2tjgrzlcqtat380vhe8awm03f58cqe49swv") diff --git a/Tests/TestUtils/TestCoordinator.swift b/Tests/TestUtils/TestCoordinator.swift index 7f4b9894..13b6d44d 100644 --- a/Tests/TestUtils/TestCoordinator.swift +++ b/Tests/TestUtils/TestCoordinator.swift @@ -35,7 +35,7 @@ class TestCoordinator { case url(urlString: String, startHeigth: BlockHeight) } - var completionHandler: ((SDKSynchronizer) -> Void)? + var completionHandler: ((SDKSynchronizer) throws -> Void)? var errorHandler: ((Error?) -> Void)? var spendingKey: SaplingExtendedSpendingKey var birthday: BlockHeight @@ -157,7 +157,7 @@ class TestCoordinator { try service.applyStaged(nextLatestHeight: height) } - func sync(completion: @escaping (SDKSynchronizer) -> Void, error: @escaping (Error?) -> Void) throws { + func sync(completion: @escaping (SDKSynchronizer) throws -> Void, error: @escaping (Error?) -> Void) throws { self.completionHandler = completion self.errorHandler = error @@ -181,7 +181,7 @@ class TestCoordinator { LoggerProxy.debug("WARNING: notification received after synchronizer was stopped") return } - self.completionHandler?(self.synchronizer) + try? self.completionHandler?(self.synchronizer) } @objc func synchronizerDisconnected(_ notification: Notification) { diff --git a/ZcashLightClientKit.podspec b/ZcashLightClientKit.podspec index 9f9c6728..6b23ca6c 100644 --- a/ZcashLightClientKit.podspec +++ b/ZcashLightClientKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'ZcashLightClientKit' - s.version = '0.16.9-beta' + s.version = '0.16.10-beta' s.summary = 'Zcash Light Client wallet SDK for iOS' s.description = <<-DESC diff --git a/changelog.md b/changelog.md index 935b6aec..eacab72e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,25 @@ +# 0.16.10-beta +- [#532] [0.16.x-beta] Download does not stop correctly + +Issue Reported: + +When the synchronizer is stopped, the processor does not cancel +the download correctly. Then when attempting to resume sync, the +synchronizer is not on `.stopped` and can't be resumed + +this doesn't appear to happen in `master` branch that uses +structured concurrency for operations. + +Fix: +This commit makes sure that the download streamer checks cancelation +before processing any block, or getting called back to report progress + +Checkpoints added: +Mainnet +```` +Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1807500.json +Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1810000.json +```` # 0.16.9-beta Checkpoints added: Mainnet