diff --git a/CHANGELOG.md b/CHANGELOG.md index 032fbc67..7c5aa343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added - `SDKSynchronizer.estimateBirthdayHeight(for date: Date)`: Get an estimated height for a given date, typically used for estimating birthday. +## Changed +- `LightWalletGRPCService` updated to use TOR connection for: fetching and submission of the transaction, getting server info, latest block height and tree state. + # 2.2.11 - 2025-04-03 ## Fixed diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift index f9259468..3bf66df7 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksViewController.swift @@ -71,8 +71,6 @@ class SyncBlocksViewController: UIViewController { case let .syncing(syncProgress, areFundsSpendable): enhancingStarted = false - print("__LD syncProgress \(syncProgress) areFundsSpendable \(areFundsSpendable)") - progressBar.progress = syncProgress progressLabel.text = "\(floor(syncProgress * 1000) / 10)% spendable: \(areFundsSpendable)" let progressText = """ diff --git a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift index 90f9038c..b5554fd5 100644 --- a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift +++ b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift @@ -778,7 +778,7 @@ extension CompactBlockProcessor { guard let self else { return } if await self.isIdle() { if await self.canStartSync() { - self.logger.debug( + await self.logger.debug( """ Timer triggered: Starting compact Block processor!. Processor State: \(await self.context.state) diff --git a/Sources/ZcashLightClientKit/Error/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index 3c052cbc..1bb1ad6f 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -420,6 +420,18 @@ public enum ZcashError: Equatable, Error { /// - `rustError` contains error generated by the rust layer. /// ZRUST0080 case rustTorLwdSubmit(_ rustError: String) + /// Error from rust layer when calling TorLwdConn.getInfo + /// - `rustError` contains error generated by the rust layer. + /// ZRUST0081 + case rustTorLwdGetInfo(_ rustError: String) + /// Error from rust layer when calling TorLwdConn.latestBlockHeight + /// - `rustError` contains error generated by the rust layer. + /// ZRUST0082 + case rustTorLwdLatestBlockHeight(_ rustError: String) + /// Error from rust layer when calling TorLwdConn.getTreeState + /// - `rustError` contains error generated by the rust layer. + /// ZRUST0083 + case rustTorLwdGetTreeState(_ rustError: String) /// SQLite query failed when fetching all accounts from the database. /// - `sqliteError` is error produced by SQLite library. /// ZADAO0001 @@ -802,6 +814,9 @@ public enum ZcashError: Equatable, Error { case .rustTorConnectToLightwalletd: return "Error from rust layer when calling TorClient.connectToLightwalletd" case .rustTorLwdFetchTransaction: return "Error from rust layer when calling TorLwdConn.fetchTransaction" case .rustTorLwdSubmit: return "Error from rust layer when calling TorLwdConn.submit" + case .rustTorLwdGetInfo: return "Error from rust layer when calling TorLwdConn.getInfo" + case .rustTorLwdLatestBlockHeight: return "Error from rust layer when calling TorLwdConn.latestBlockHeight" + case .rustTorLwdGetTreeState: return "Error from rust layer when calling TorLwdConn.getTreeState" case .accountDAOGetAll: return "SQLite query failed when fetching all accounts from the database." case .accountDAOGetAllCantDecode: return "Fetched accounts from SQLite but can't decode them." case .accountDAOFindBy: return "SQLite query failed when seaching for accounts in the database." @@ -1001,6 +1016,9 @@ public enum ZcashError: Equatable, Error { case .rustTorConnectToLightwalletd: return .rustTorConnectToLightwalletd case .rustTorLwdFetchTransaction: return .rustTorLwdFetchTransaction case .rustTorLwdSubmit: return .rustTorLwdSubmit + case .rustTorLwdGetInfo: return .rustTorLwdGetInfo + case .rustTorLwdLatestBlockHeight: return .rustTorLwdLatestBlockHeight + case .rustTorLwdGetTreeState: return .rustTorLwdGetTreeState case .accountDAOGetAll: return .accountDAOGetAll case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode case .accountDAOFindBy: return .accountDAOFindBy diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift index 6d17d791..919cce7f 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -223,6 +223,12 @@ public enum ZcashErrorCode: String { case rustTorLwdFetchTransaction = "ZRUST0079" /// Error from rust layer when calling TorLwdConn.submit case rustTorLwdSubmit = "ZRUST0080" + /// Error from rust layer when calling TorLwdConn.getInfo + case rustTorLwdGetInfo = "ZRUST0081" + /// Error from rust layer when calling TorLwdConn.latestBlockHeight + case rustTorLwdLatestBlockHeight = "ZRUST0082" + /// Error from rust layer when calling TorLwdConn.getTreeState + case rustTorLwdGetTreeState = "ZRUST0083" /// SQLite query failed when fetching all accounts from the database. case accountDAOGetAll = "ZADAO0001" /// Fetched accounts from SQLite but can't decode them. diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift index 21497bd4..52561797 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift @@ -442,6 +442,18 @@ enum ZcashErrorDefinition { /// - `rustError` contains error generated by the rust layer. // sourcery: code="ZRUST0080" case rustTorLwdSubmit(_ rustError: String) + /// Error from rust layer when calling TorLwdConn.getInfo + /// - `rustError` contains error generated by the rust layer. + // sourcery: code="ZRUST0081" + case rustTorLwdGetInfo(_ rustError: String) + /// Error from rust layer when calling TorLwdConn.latestBlockHeight + /// - `rustError` contains error generated by the rust layer. + // sourcery: code="ZRUST0082" + case rustTorLwdLatestBlockHeight(_ rustError: String) + /// Error from rust layer when calling TorLwdConn.getTreeState + /// - `rustError` contains error generated by the rust layer. + // sourcery: code="ZRUST0083" + case rustTorLwdGetTreeState(_ rustError: String) // MARK: - Account DAO diff --git a/Sources/ZcashLightClientKit/Initializer.swift b/Sources/ZcashLightClientKit/Initializer.swift index a96a8bdb..b78a577d 100644 --- a/Sources/ZcashLightClientKit/Initializer.swift +++ b/Sources/ZcashLightClientKit/Initializer.swift @@ -42,8 +42,7 @@ public struct LightWalletEndpoint { } var urlString: String { - return String( - format: "%@://%@:%d", secure ? "https" : "http", host, port) + String(format: "%@://%@:%d", secure ? "https" : "http", host, port) } } @@ -54,7 +53,7 @@ public struct SaplingParamsSourceURL { public let outputParamFileURL: URL public static var `default`: SaplingParamsSourceURL { - return SaplingParamsSourceURL(spendParamFileURL: ZcashSDK.spendParamFileURL, outputParamFileURL: ZcashSDK.outputParamFileURL) + SaplingParamsSourceURL(spendParamFileURL: ZcashSDK.spendParamFileURL, outputParamFileURL: ZcashSDK.outputParamFileURL) } } diff --git a/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift b/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift index 54afeb17..52394211 100644 --- a/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift +++ b/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift @@ -61,6 +61,7 @@ class LightWalletGRPCService { let singleCallTimeout: TimeLimit let streamingCallTimeout: TimeLimit var latestBlockHeightProvider: LatestBlockHeightProvider = LiveLatestBlockHeightProvider() + let torConn: TorLwdConn? var connectionStateChange: ((_ from: ConnectionState, _ to: ConnectionState) -> Void)? { get { connectionManager.connectionStateChange } @@ -69,13 +70,14 @@ class LightWalletGRPCService { let queue: DispatchQueue - convenience init(endpoint: LightWalletEndpoint) { + convenience init(endpoint: LightWalletEndpoint, torURL: URL?) { self.init( host: endpoint.host, port: endpoint.port, secure: endpoint.secure, singleCallTimeout: endpoint.singleCallTimeoutInMillis, - streamingCallTimeout: endpoint.streamingCallTimeoutInMillis + streamingCallTimeout: endpoint.streamingCallTimeoutInMillis, + torURL: torURL ) } @@ -91,7 +93,8 @@ class LightWalletGRPCService { port: Int = 9067, secure: Bool = true, singleCallTimeout: Int64, - streamingCallTimeout: Int64 + streamingCallTimeout: Int64, + torURL: URL? ) { self.connectionManager = ConnectionStatusManager() self.queue = DispatchQueue.init(label: "LightWalletGRPCService") @@ -114,6 +117,13 @@ class LightWalletGRPCService { timeLimit: self.singleCallTimeout ) ) + + var tor: TorClient? = nil + if let torURL { + tor = try? TorClient(torDir: torURL) + } + let endpointString = String(format: "%@://%@:%d", secure ? "https" : "http", host, port) + self.torConn = try? tor?.connectToLightwalletd(endpoint: endpointString) } deinit { @@ -151,15 +161,23 @@ class LightWalletGRPCService { extension LightWalletGRPCService: LightWalletService { func getInfo() async throws -> LightWalletdInfo { do { - return try await compactTxStreamer.getLightdInfo(Empty()) + if let torConn { + return try torConn.getInfo() + } else { + return try await compactTxStreamer.getLightdInfo(Empty()) + } } catch { let serviceError = error.mapToServiceError() throw ZcashError.serviceGetInfoFailed(serviceError) } } - + func latestBlockHeight() async throws -> BlockHeight { - try await latestBlockHeightProvider.latestBlockHeight(streamer: compactTxStreamer) + if let torConn { + return try torConn.latestBlockHeight() + } else { + return try await latestBlockHeightProvider.latestBlockHeight(streamer: compactTxStreamer) + } } func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream { @@ -176,11 +194,15 @@ extension LightWalletGRPCService: LightWalletService { } } } - + func submit(spendTransaction: Data) async throws -> LightWalletServiceResponse { do { - let transaction = RawTransaction.with { $0.data = spendTransaction } - return try await compactTxStreamer.sendTransaction(transaction) + if let torConn { + return try torConn.submit(spendTransaction: spendTransaction) + } else { + let transaction = RawTransaction.with { $0.data = spendTransaction } + return try await compactTxStreamer.sendTransaction(transaction) + } } catch { let serviceError = error.mapToServiceError() throw ZcashError.serviceSubmitFailed(serviceError) @@ -188,37 +210,41 @@ extension LightWalletGRPCService: LightWalletService { } func fetchTransaction(txId: Data) async throws -> (tx: ZcashTransaction.Fetched?, status: TransactionStatus) { - var txFilter = TxFilter() - txFilter.hash = txId - - do { - let rawTx = try await compactTxStreamer.getTransaction(txFilter) + if let torConn { + return try torConn.fetchTransaction(txId: txId) + } else { + var txFilter = TxFilter() + txFilter.hash = txId - let isNotMined = rawTx.height == 0 || rawTx.height > UInt32.max - - return ( - tx: - ZcashTransaction.Fetched( - rawID: txId, - minedHeight: isNotMined ? nil : UInt32(rawTx.height), - raw: rawTx.data - ), - status: isNotMined ? .notInMainChain : .mined(Int(rawTx.height)) - ) - } catch let error as GRPCStatus { - if error.makeGRPCStatus().code == .notFound { - return (tx: nil, .txidNotRecognized) - } else if let notFound = error.message?.contains("Transaction not found"), notFound { - return (tx: nil, .txidNotRecognized) - } else if let notFound = error.message?.contains("No such mempool or blockchain transaction. Use gettransaction for wallet transactions."), notFound { - return (tx: nil, .txidNotRecognized) - } else { + do { + let rawTx = try await compactTxStreamer.getTransaction(txFilter) + + let isNotMined = rawTx.height == 0 || rawTx.height > UInt32.max + + return ( + tx: + ZcashTransaction.Fetched( + rawID: txId, + minedHeight: isNotMined ? nil : UInt32(rawTx.height), + raw: rawTx.data + ), + status: isNotMined ? .notInMainChain : .mined(Int(rawTx.height)) + ) + } catch let error as GRPCStatus { + if error.makeGRPCStatus().code == .notFound { + return (tx: nil, .txidNotRecognized) + } else if let notFound = error.message?.contains("Transaction not found"), notFound { + return (tx: nil, .txidNotRecognized) + } else if let notFound = error.message?.contains("No such mempool or blockchain transaction. Use gettransaction for wallet transactions."), notFound { + return (tx: nil, .txidNotRecognized) + } else { + let serviceError = error.mapToServiceError() + throw ZcashError.serviceFetchTransactionFailed(serviceError) + } + } catch { let serviceError = error.mapToServiceError() throw ZcashError.serviceFetchTransactionFailed(serviceError) } - } catch { - let serviceError = error.mapToServiceError() - throw ZcashError.serviceFetchTransactionFailed(serviceError) } } @@ -299,7 +325,11 @@ extension LightWalletGRPCService: LightWalletService { } func getTreeState(_ id: BlockID) async throws -> TreeState { - try await compactTxStreamer.getTreeState(id) + if let torConn { + return try torConn.getTreeState(height: BlockHeight(id.height)) + } else { + return try await compactTxStreamer.getTreeState(id) + } } func getTaddressTxids(_ request: TransparentAddressBlockFilter) -> AsyncThrowingStream { diff --git a/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift b/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift index dcce7b5f..ce3b258a 100644 --- a/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift +++ b/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift @@ -136,9 +136,10 @@ protocol LightWalletServiceResponse { struct LightWalletServiceFactory { let endpoint: LightWalletEndpoint + let torURL: URL? func make() -> LightWalletService { - return LightWalletGRPCService(endpoint: endpoint) + return LightWalletGRPCService(endpoint: endpoint, torURL: torURL) } } diff --git a/Sources/ZcashLightClientKit/Rust/ZcashKeyDerivationBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashKeyDerivationBackend.swift index eec6696b..3cffc289 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashKeyDerivationBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashKeyDerivationBackend.swift @@ -101,7 +101,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding { ) guard let ffiAddressPtr else { - throw ZcashError.rustDeriveAddressFromUfvk(ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveAddressFromUfvk` failed with unknown error")) + throw ZcashError.rustDeriveAddressFromUfvk(ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveAddressFromUfvk` failed with unknown error") + ) } defer { zcashlc_free_ffi_address(ffiAddressPtr) } @@ -126,7 +127,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding { guard let boxedSlice = boxedSlicePtr?.pointee else { throw ZcashError.rustDeriveUnifiedSpendingKey( - ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveUnifiedSpendingKey` failed with unknown error")) + ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveUnifiedSpendingKey` failed with unknown error") + ) } return boxedSlice.unsafeToUnifiedSpendingKey(network: networkType) @@ -207,7 +209,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding { guard let key = boxedSlicePtr?.pointee else { throw ZcashError.rustDeriveArbitraryWalletKey( - ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryWalletKey` failed with unknown error")) + ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryWalletKey` failed with unknown error") + ) } return key.ptr.toByteArray( @@ -237,7 +240,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding { guard let key = boxedSlicePtr?.pointee else { throw ZcashError.rustDeriveArbitraryAccountKey( - ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryAccountKey` failed with unknown error")) + ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryAccountKey` failed with unknown error") + ) } return key.ptr.toByteArray( diff --git a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift index 8db21e0f..6c274935 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift @@ -76,7 +76,7 @@ enum Dependencies { } container.register(type: LightWalletService.self, isSingleton: true) { _ in - LightWalletGRPCService(endpoint: endpoint) + LightWalletGRPCService(endpoint: endpoint, torURL: urls.torDirURL) } container.register(type: TransactionRepository.self, isSingleton: true) { _ in diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index 3bf0d77b..abedff1b 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -180,8 +180,8 @@ public class SDKSynchronizer: Synchronizer { var areFundsSpendable = false if let scanProgress = walletSummary?.scanProgress { - let composedNumerator: Float = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0) - let composedDenominator: Float = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0) + let composedNumerator = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0) + let composedDenominator = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0) let progress: Float if composedDenominator == 0 { @@ -749,7 +749,8 @@ public class SDKSynchronizer: Synchronizer { port: $0.port, secure: $0.secure, singleCallTimeout: 5000, - streamingCallTimeout: Int64(fetchThresholdSeconds) * 1000 + streamingCallTimeout: Int64(fetchThresholdSeconds) * 1000, + torURL: initializer.torDirURL ), url: "\($0.host):\($0.port)" ) @@ -907,13 +908,14 @@ public class SDKSynchronizer: Synchronizer { // Validation of the server is first because any custom endpoint can be passed here // Extra instance of the service is created with lower timeout ofr a single call - initializer.container.register(type: LightWalletService.self, isSingleton: true) { _ in + initializer.container.register(type: LightWalletService.self, isSingleton: true) { [torURL = initializer.torDirURL] _ in LightWalletGRPCService( host: endpoint.host, port: endpoint.port, secure: endpoint.secure, singleCallTimeout: 5000, - streamingCallTimeout: endpoint.streamingCallTimeoutInMillis + streamingCallTimeout: endpoint.streamingCallTimeoutInMillis, + torURL: torURL ) } @@ -934,8 +936,8 @@ public class SDKSynchronizer: Synchronizer { // SWITCH TO NEW ENDPOINT // LightWalletService dependency update - initializer.container.register(type: LightWalletService.self, isSingleton: true) { _ in - LightWalletGRPCService(endpoint: endpoint) + initializer.container.register(type: LightWalletService.self, isSingleton: true) { [torURL = initializer.torDirURL] _ in + LightWalletGRPCService(endpoint: endpoint, torURL: torURL) } // DEPENDENCIES diff --git a/Sources/ZcashLightClientKit/Tor/TorClient.swift b/Sources/ZcashLightClientKit/Tor/TorClient.swift index 77f6361b..88ad18b4 100644 --- a/Sources/ZcashLightClientKit/Tor/TorClient.swift +++ b/Sources/ZcashLightClientKit/Tor/TorClient.swift @@ -12,7 +12,7 @@ public class TorClient { private let runtime: OpaquePointer public var cachedFiatCurrencyResult: FiatCurrencyResult? - init(torDir: URL) async throws { + init(torDir: URL) throws { // Ensure that the directory exists. let fileManager = FileManager() if !fileManager.fileExists(atPath: torDir.path) { @@ -41,20 +41,19 @@ public class TorClient { zcashlc_free_tor_runtime(runtime) } - public func isolatedClient() async throws -> TorClient { + public func isolatedClient() throws -> TorClient { let isolatedPtr = zcashlc_tor_isolated_client(runtime) guard let isolatedPtr else { throw ZcashError.rustTorIsolatedClient( - lastErrorMessage( - fallback: - "`TorClient.isolatedClient` failed with unknown error")) + lastErrorMessage(fallback: "`TorClient.isolatedClient` failed with unknown error") + ) } return TorClient(runtimePtr: isolatedPtr) } - public func getExchangeRateUSD() async throws -> FiatCurrencyResult { + public func getExchangeRateUSD() throws -> FiatCurrencyResult { let rate = zcashlc_get_exchange_rate_usd(runtime) if rate.is_sign_negative { @@ -64,8 +63,10 @@ public class TorClient { let newValue = FiatCurrencyResult( date: Date(), rate: NSDecimalNumber( - mantissa: rate.mantissa, exponent: rate.exponent, - isNegative: rate.is_sign_negative), + mantissa: rate.mantissa, + exponent: rate.exponent, + isNegative: rate.is_sign_negative + ), state: .success ) @@ -74,23 +75,19 @@ public class TorClient { return newValue } - public func connectToLightwalletd(endpoint: String) async throws - -> TorLwdConn - { + public func connectToLightwalletd(endpoint: String) throws -> TorLwdConn { guard !endpoint.containsCStringNullBytesBeforeStringEnding() else { - throw ZcashError.rustTorConnectToLightwalletd( - "endpoint string contains null bytes") + throw ZcashError.rustTorConnectToLightwalletd("endpoint string contains null bytes") } let lwdConnPtr = zcashlc_tor_connect_to_lightwalletd( - runtime, [CChar](endpoint.utf8CString)) + runtime, [CChar](endpoint.utf8CString) + ) guard let lwdConnPtr else { throw ZcashError.rustTorConnectToLightwalletd( - lastErrorMessage( - fallback: - "`TorClient.connectToLightwalletd` failed with unknown error" - )) + lastErrorMessage(fallback: "`TorClient.connectToLightwalletd` failed with unknown error") + ) } return TorLwdConn(connPtr: lwdConnPtr) @@ -100,7 +97,7 @@ public class TorClient { public class TorLwdConn { private let conn: OpaquePointer - fileprivate init(connPtr: OpaquePointer) { + init(connPtr: OpaquePointer) { conn = connPtr } @@ -111,9 +108,7 @@ public class TorLwdConn { /// Submits a raw transaction over lightwalletd. /// - Parameter spendTransaction: data representing the transaction to be sent /// - Throws: `serviceSubmitFailed` when GRPC call fails. - func submit(spendTransaction: Data) async throws - -> LightWalletServiceResponse - { + func submit(spendTransaction: Data) throws -> LightWalletServiceResponse { let success = zcashlc_tor_lwd_conn_submit_transaction( conn, spendTransaction.bytes, @@ -121,18 +116,21 @@ public class TorLwdConn { ) var response = SendResponse() + if !success { - let err = lastErrorMessage( - fallback: "`TorLwdConn.submit` failed with unknown error") - if err.hasPrefix("Failed to submit transaction (") - && err.contains(")") - { - let startOfCode = err.firstIndex(of: "(")! - let endOfCode = err.firstIndex(of: ")")! - let errorCode = Int32( - err[err.index(startOfCode, offsetBy: 1).. ( - tx: ZcashTransaction.Fetched?, status: TransactionStatus - ) { + func fetchTransaction(txId: Data) throws -> (tx: ZcashTransaction.Fetched?, status: TransactionStatus) { guard txId.count == 32 else { throw ZcashError.rustGetMemoInvalidTxIdLength } @@ -161,10 +158,7 @@ public class TorLwdConn { guard let txPtr else { throw ZcashError.rustTorLwdFetchTransaction( - lastErrorMessage( - fallback: - "`TorLwdConn.fetchTransaction` failed with unknown error" - ) + lastErrorMessage(fallback: "`TorLwdConn.fetchTransaction` failed with unknown error") ) } @@ -185,4 +179,81 @@ public class TorLwdConn { status: isNotMined ? .notInMainChain : .mined(Int(height)) ) } + + /// Gets a lightwalletd server info + /// - Returns: LightWalletdInfo + func getInfo() throws -> LightWalletdInfo { + let infoPtr = zcashlc_tor_lwd_conn_get_info(conn) + + guard let infoPtr else { + throw ZcashError.rustTorLwdGetInfo( + lastErrorMessage(fallback: "`TorLwdConn.getInfo` failed with unknown error") + ) + } + + defer { zcashlc_free_boxed_slice(infoPtr) } + + let slice = infoPtr.pointee + guard let rawPtr = slice.ptr else { + throw ZcashError.rustTorLwdGetInfo("`TorLwdConn.getInfo` Null pointer in FfiBoxedSlice") + } + + let buffer = UnsafeBufferPointer(start: rawPtr, count: Int(slice.len)) + let data = Data(buffer: buffer) + + do { + let info = try LightdInfo(serializedBytes: data) + return info + } catch { + throw ZcashError.rustTorLwdGetInfo("`TorLwdConn.getInfo` Failed to decode protobuf LightdInfo: \(error)") + } + } + + /// Gets a chain tip of the blockchain (latest block height) + /// - Returns: BlockHeight + func latestBlockHeight() throws -> BlockHeight { + var height: UInt32 = 0 + + let blockIDPtr = zcashlc_tor_lwd_conn_latest_block(conn, &height) + + guard let blockIDPtr else { + throw ZcashError.rustTorLwdLatestBlockHeight( + lastErrorMessage(fallback: "`TorLwdConn.latestBlockHeight` failed with unknown error") + ) + } + + defer { zcashlc_free_boxed_slice(blockIDPtr) } + + return BlockHeight(height) + } + + /// Gets a tree state for a given height + /// - Parameter height: heght for what a tree state is requested + /// - Returns: TreeState + func getTreeState(height: BlockHeight) throws -> TreeState { + let treeStatePtr = zcashlc_tor_lwd_conn_get_tree_state(conn, UInt32(height)) + + guard let treeStatePtr else { + throw ZcashError.rustTorLwdGetTreeState( + lastErrorMessage(fallback: "`TorLwdConn.getTreeState` failed with unknown error") + ) + } + + defer { zcashlc_free_boxed_slice(treeStatePtr) } + + let slice = treeStatePtr.pointee + guard let rawPtr = slice.ptr else { + throw ZcashError.rustTorLwdGetTreeState("`TorLwdConn.getTreeState` Null pointer in FfiBoxedSlice") + } + + let buffer = UnsafeBufferPointer(start: rawPtr, count: Int(slice.len)) + let data = Data(buffer: buffer) + + do { + let treeState = try TreeState(serializedBytes: data) + return treeState + } catch { + throw ZcashError.rustTorLwdGetTreeState("`TorLwdConn.getTreeState` Failed to decode protobuf TreeState: \(error)") + } + } } diff --git a/Tests/DarksideTests/BlockDownloaderTests.swift b/Tests/DarksideTests/BlockDownloaderTests.swift index dd96e847..61d874ba 100644 --- a/Tests/DarksideTests/BlockDownloaderTests.swift +++ b/Tests/DarksideTests/BlockDownloaderTests.swift @@ -27,7 +27,7 @@ class BlockDownloaderTests: XCTestCase { try await super.setUp() testTempDirectory = Environment.uniqueTestTempDirectory - service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() + service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default, torURL: nil).make() rustBackend = ZcashRustBackend.makeForTests( fsBlockDbRoot: testTempDirectory, diff --git a/Tests/DarksideTests/TransactionEnhancementTests.swift b/Tests/DarksideTests/TransactionEnhancementTests.swift index 57821f1f..f3612851 100644 --- a/Tests/DarksideTests/TransactionEnhancementTests.swift +++ b/Tests/DarksideTests/TransactionEnhancementTests.swift @@ -97,7 +97,7 @@ class TransactionEnhancementTests: ZcashTestCase { return } - let service = DarksideWalletService() + let service = DarksideWalletService(torURL: nil) darksideWalletService = service let storage = FSCompactBlockRepository( diff --git a/Tests/NetworkTests/BlockStreamingTest.swift b/Tests/NetworkTests/BlockStreamingTest.swift index 54398a08..4f5d52c2 100644 --- a/Tests/NetworkTests/BlockStreamingTest.swift +++ b/Tests/NetworkTests/BlockStreamingTest.swift @@ -52,7 +52,7 @@ class BlockStreamingTest: ZcashTestCase { singleCallTimeoutInMillis: 10000, streamingCallTimeoutInMillis: 10000 ) - let service = LightWalletServiceFactory(endpoint: endpoint).make() + let service = LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make() latestBlockHeight = try await service.latestBlockHeight() startHeight = latestBlockHeight - 10_000 @@ -77,7 +77,7 @@ class BlockStreamingTest: ZcashTestCase { streamingCallTimeoutInMillis: timeout ) self.endpoint = endpoint - service = LightWalletServiceFactory(endpoint: endpoint).make() + service = LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make() storage = FSCompactBlockRepository( fsBlockDbRoot: testTempDirectory, metadataStore: FSMetadataStore.live( @@ -97,7 +97,7 @@ class BlockStreamingTest: ZcashTestCase { ) mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in - LightWalletServiceFactory(endpoint: endpoint).make() + LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make() } let transactionRepositoryMock = TransactionRepositoryMock() diff --git a/Tests/NetworkTests/CompactBlockProcessorTests.swift b/Tests/NetworkTests/CompactBlockProcessorTests.swift index 06cec319..5174d194 100644 --- a/Tests/NetworkTests/CompactBlockProcessorTests.swift +++ b/Tests/NetworkTests/CompactBlockProcessorTests.swift @@ -43,7 +43,7 @@ class CompactBlockProcessorTests: ZcashTestCase { network: ZcashNetworkBuilder.network(for: .testnet) ) - let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() + let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make() let service = MockLightWalletService( latestBlockHeight: mockLatestHeight, service: liveService diff --git a/Tests/NetworkTests/CompactBlockReorgTests.swift b/Tests/NetworkTests/CompactBlockReorgTests.swift index b4b13e51..5a1e12a3 100644 --- a/Tests/NetworkTests/CompactBlockReorgTests.swift +++ b/Tests/NetworkTests/CompactBlockReorgTests.swift @@ -44,7 +44,7 @@ class CompactBlockReorgTests: ZcashTestCase { network: ZcashNetworkBuilder.network(for: .testnet) ) - let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() + let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make() let service = MockLightWalletService( latestBlockHeight: mockLatestHeight, service: liveService diff --git a/Tests/NetworkTests/DownloadTests.swift b/Tests/NetworkTests/DownloadTests.swift index 56a7450d..1a07290b 100644 --- a/Tests/NetworkTests/DownloadTests.swift +++ b/Tests/NetworkTests/DownloadTests.swift @@ -46,7 +46,7 @@ class DownloadTests: ZcashTestCase { ZcashRustBackend.makeForTests(fsBlockDbRoot: self.testTempDirectory, networkType: self.network.networkType) } mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in - LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() + LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make() } let storage = mockContainer.resolve(CompactBlockRepository.self) try await storage.create() diff --git a/Tests/NetworkTests/LightWalletServiceTests.swift b/Tests/NetworkTests/LightWalletServiceTests.swift index be33e85f..057dd1d0 100644 --- a/Tests/NetworkTests/LightWalletServiceTests.swift +++ b/Tests/NetworkTests/LightWalletServiceTests.swift @@ -19,7 +19,7 @@ class LightWalletServiceTests: XCTestCase { override func setUp() { // Put setup code here. This method is called before the invocation of each test method in the class. super.setUp() - service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() + service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make() } override func tearDownWithError() throws { diff --git a/Tests/NetworkTests/TorClientTests.swift b/Tests/NetworkTests/TorClientTests.swift index 4fac4007..eedf2f80 100644 --- a/Tests/NetworkTests/TorClientTests.swift +++ b/Tests/NetworkTests/TorClientTests.swift @@ -14,23 +14,23 @@ import XCTest class TorClientTests: ZcashTestCase { let network: ZcashNetwork = ZcashNetworkBuilder.network(for: .testnet) - func testLwdCanFetchAndSubmitTx() async throws { + func testLwdCanFetchAndSubmitTx() throws { // Spin up a new Tor client. - let client = try await TorClient(torDir: testTempDirectory) + let client = try TorClient(torDir: testTempDirectory) // Connect to a testnet lightwalletd server. - let lwdConn = try await client.connectToLightwalletd( + let lwdConn = try client.connectToLightwalletd( endpoint: LightWalletEndpointBuilder.publicTestnet.urlString) // Fetch a known testnet transaction. let txId = "9e309d29a99f06e6dcc7aee91dca23c0efc2cf5083cc483463ddbee19c1fadf1" .toTxIdString().hexadecimal! - let (tx, status) = try await lwdConn.fetchTransaction(txId: txId) + let (tx, status) = try lwdConn.fetchTransaction(txId: txId) XCTAssertEqual(status, .mined(1_234_567)) // We should fail to resubmit the already-mined transaction. - let result = try await lwdConn.submit(spendTransaction: tx!.raw) + let result = try lwdConn.submit(spendTransaction: tx!.raw) XCTAssertEqual(result.errorCode, -25) XCTAssertEqual( result.errorMessage, diff --git a/Tests/TestUtils/DarkSideWalletService.swift b/Tests/TestUtils/DarkSideWalletService.swift index a7b9ca50..6dad38eb 100644 --- a/Tests/TestUtils/DarkSideWalletService.swift +++ b/Tests/TestUtils/DarkSideWalletService.swift @@ -50,9 +50,9 @@ class DarksideWalletService: LightWalletService { var service: LightWalletService var darksideService: DarksideStreamerNIOClient - init(endpoint: LightWalletEndpoint) { + init(endpoint: LightWalletEndpoint, torURL: URL?) { self.channel = ChannelProvider().channel(endpoint: endpoint) - self.service = LightWalletServiceFactory(endpoint: endpoint).make() + self.service = LightWalletServiceFactory(endpoint: endpoint, torURL: torURL).make() self.darksideService = DarksideStreamerNIOClient(channel: channel) } @@ -62,8 +62,8 @@ class DarksideWalletService: LightWalletService { self.service = service } - convenience init() { - self.init(endpoint: LightWalletEndpointBuilder.default) + convenience init(torURL: URL?) { + self.init(endpoint: LightWalletEndpointBuilder.default, torURL: torURL) } func blockStream(startHeight: BlockHeight, endHeight: BlockHeight) -> AsyncThrowingStream { diff --git a/Tests/TestUtils/TestCoordinator.swift b/Tests/TestUtils/TestCoordinator.swift index 922970d1..85f8a315 100644 --- a/Tests/TestUtils/TestCoordinator.swift +++ b/Tests/TestUtils/TestCoordinator.swift @@ -93,7 +93,7 @@ class TestCoordinator { self.birthday = walletBirthday self.network = network - let liveService = LightWalletServiceFactory(endpoint: endpoint).make() + let liveService = LightWalletServiceFactory(endpoint: endpoint, torURL: databases.torDir).make() self.service = DarksideWalletService(endpoint: endpoint, service: liveService) self.synchronizer = SDKSynchronizer(initializer: initializer) subscribeToState(synchronizer: self.synchronizer)