[#1469] Use Tor for single-shot lightwalletd requests

- TorLwdConn refactored to resolve SwiftLint issues and best practives
- TorLwdConn getInfo, getTreeState and latestBlockHeight methids implemented
- LightWalletGRPCService enhanced to use available TOR methods
- Code cleaned up
- Changelog updated
- documented tor connection methods
- torConnection initialization when fails, fallback to classic compactTxStreamer
This commit is contained in:
Lukas Korba 2025-04-15 15:07:28 +02:00
parent 1a2db6b167
commit 16a80d2efb
23 changed files with 259 additions and 115 deletions

View File

@ -9,6 +9,9 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Added ## Added
- `SDKSynchronizer.estimateBirthdayHeight(for date: Date)`: Get an estimated height for a given date, typically used for estimating birthday. - `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 # 2.2.11 - 2025-04-03
## Fixed ## Fixed

View File

@ -71,8 +71,6 @@ class SyncBlocksViewController: UIViewController {
case let .syncing(syncProgress, areFundsSpendable): case let .syncing(syncProgress, areFundsSpendable):
enhancingStarted = false enhancingStarted = false
print("__LD syncProgress \(syncProgress) areFundsSpendable \(areFundsSpendable)")
progressBar.progress = syncProgress progressBar.progress = syncProgress
progressLabel.text = "\(floor(syncProgress * 1000) / 10)% spendable: \(areFundsSpendable)" progressLabel.text = "\(floor(syncProgress * 1000) / 10)% spendable: \(areFundsSpendable)"
let progressText = """ let progressText = """

View File

@ -778,7 +778,7 @@ extension CompactBlockProcessor {
guard let self else { return } guard let self else { return }
if await self.isIdle() { if await self.isIdle() {
if await self.canStartSync() { if await self.canStartSync() {
self.logger.debug( await self.logger.debug(
""" """
Timer triggered: Starting compact Block processor!. Timer triggered: Starting compact Block processor!.
Processor State: \(await self.context.state) Processor State: \(await self.context.state)

View File

@ -420,6 +420,18 @@ public enum ZcashError: Equatable, Error {
/// - `rustError` contains error generated by the rust layer. /// - `rustError` contains error generated by the rust layer.
/// ZRUST0080 /// ZRUST0080
case rustTorLwdSubmit(_ rustError: String) 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. /// SQLite query failed when fetching all accounts from the database.
/// - `sqliteError` is error produced by SQLite library. /// - `sqliteError` is error produced by SQLite library.
/// ZADAO0001 /// ZADAO0001
@ -802,6 +814,9 @@ public enum ZcashError: Equatable, Error {
case .rustTorConnectToLightwalletd: return "Error from rust layer when calling TorClient.connectToLightwalletd" case .rustTorConnectToLightwalletd: return "Error from rust layer when calling TorClient.connectToLightwalletd"
case .rustTorLwdFetchTransaction: return "Error from rust layer when calling TorLwdConn.fetchTransaction" case .rustTorLwdFetchTransaction: return "Error from rust layer when calling TorLwdConn.fetchTransaction"
case .rustTorLwdSubmit: return "Error from rust layer when calling TorLwdConn.submit" 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 .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 .accountDAOGetAllCantDecode: return "Fetched accounts from SQLite but can't decode them."
case .accountDAOFindBy: return "SQLite query failed when seaching for accounts in the database." 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 .rustTorConnectToLightwalletd: return .rustTorConnectToLightwalletd
case .rustTorLwdFetchTransaction: return .rustTorLwdFetchTransaction case .rustTorLwdFetchTransaction: return .rustTorLwdFetchTransaction
case .rustTorLwdSubmit: return .rustTorLwdSubmit case .rustTorLwdSubmit: return .rustTorLwdSubmit
case .rustTorLwdGetInfo: return .rustTorLwdGetInfo
case .rustTorLwdLatestBlockHeight: return .rustTorLwdLatestBlockHeight
case .rustTorLwdGetTreeState: return .rustTorLwdGetTreeState
case .accountDAOGetAll: return .accountDAOGetAll case .accountDAOGetAll: return .accountDAOGetAll
case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode
case .accountDAOFindBy: return .accountDAOFindBy case .accountDAOFindBy: return .accountDAOFindBy

View File

@ -223,6 +223,12 @@ public enum ZcashErrorCode: String {
case rustTorLwdFetchTransaction = "ZRUST0079" case rustTorLwdFetchTransaction = "ZRUST0079"
/// Error from rust layer when calling TorLwdConn.submit /// Error from rust layer when calling TorLwdConn.submit
case rustTorLwdSubmit = "ZRUST0080" 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. /// SQLite query failed when fetching all accounts from the database.
case accountDAOGetAll = "ZADAO0001" case accountDAOGetAll = "ZADAO0001"
/// Fetched accounts from SQLite but can't decode them. /// Fetched accounts from SQLite but can't decode them.

View File

@ -442,6 +442,18 @@ enum ZcashErrorDefinition {
/// - `rustError` contains error generated by the rust layer. /// - `rustError` contains error generated by the rust layer.
// sourcery: code="ZRUST0080" // sourcery: code="ZRUST0080"
case rustTorLwdSubmit(_ rustError: String) 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 // MARK: - Account DAO

View File

@ -42,8 +42,7 @@ public struct LightWalletEndpoint {
} }
var urlString: String { var urlString: String {
return String( String(format: "%@://%@:%d", secure ? "https" : "http", host, port)
format: "%@://%@:%d", secure ? "https" : "http", host, port)
} }
} }
@ -54,7 +53,7 @@ public struct SaplingParamsSourceURL {
public let outputParamFileURL: URL public let outputParamFileURL: URL
public static var `default`: SaplingParamsSourceURL { public static var `default`: SaplingParamsSourceURL {
return SaplingParamsSourceURL(spendParamFileURL: ZcashSDK.spendParamFileURL, outputParamFileURL: ZcashSDK.outputParamFileURL) SaplingParamsSourceURL(spendParamFileURL: ZcashSDK.spendParamFileURL, outputParamFileURL: ZcashSDK.outputParamFileURL)
} }
} }

View File

@ -61,6 +61,7 @@ class LightWalletGRPCService {
let singleCallTimeout: TimeLimit let singleCallTimeout: TimeLimit
let streamingCallTimeout: TimeLimit let streamingCallTimeout: TimeLimit
var latestBlockHeightProvider: LatestBlockHeightProvider = LiveLatestBlockHeightProvider() var latestBlockHeightProvider: LatestBlockHeightProvider = LiveLatestBlockHeightProvider()
let torConn: TorLwdConn?
var connectionStateChange: ((_ from: ConnectionState, _ to: ConnectionState) -> Void)? { var connectionStateChange: ((_ from: ConnectionState, _ to: ConnectionState) -> Void)? {
get { connectionManager.connectionStateChange } get { connectionManager.connectionStateChange }
@ -69,13 +70,14 @@ class LightWalletGRPCService {
let queue: DispatchQueue let queue: DispatchQueue
convenience init(endpoint: LightWalletEndpoint) { convenience init(endpoint: LightWalletEndpoint, torURL: URL?) {
self.init( self.init(
host: endpoint.host, host: endpoint.host,
port: endpoint.port, port: endpoint.port,
secure: endpoint.secure, secure: endpoint.secure,
singleCallTimeout: endpoint.singleCallTimeoutInMillis, singleCallTimeout: endpoint.singleCallTimeoutInMillis,
streamingCallTimeout: endpoint.streamingCallTimeoutInMillis streamingCallTimeout: endpoint.streamingCallTimeoutInMillis,
torURL: torURL
) )
} }
@ -91,7 +93,8 @@ class LightWalletGRPCService {
port: Int = 9067, port: Int = 9067,
secure: Bool = true, secure: Bool = true,
singleCallTimeout: Int64, singleCallTimeout: Int64,
streamingCallTimeout: Int64 streamingCallTimeout: Int64,
torURL: URL?
) { ) {
self.connectionManager = ConnectionStatusManager() self.connectionManager = ConnectionStatusManager()
self.queue = DispatchQueue.init(label: "LightWalletGRPCService") self.queue = DispatchQueue.init(label: "LightWalletGRPCService")
@ -114,6 +117,13 @@ class LightWalletGRPCService {
timeLimit: self.singleCallTimeout 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 { deinit {
@ -151,15 +161,23 @@ class LightWalletGRPCService {
extension LightWalletGRPCService: LightWalletService { extension LightWalletGRPCService: LightWalletService {
func getInfo() async throws -> LightWalletdInfo { func getInfo() async throws -> LightWalletdInfo {
do { do {
return try await compactTxStreamer.getLightdInfo(Empty()) if let torConn {
return try torConn.getInfo()
} else {
return try await compactTxStreamer.getLightdInfo(Empty())
}
} catch { } catch {
let serviceError = error.mapToServiceError() let serviceError = error.mapToServiceError()
throw ZcashError.serviceGetInfoFailed(serviceError) throw ZcashError.serviceGetInfoFailed(serviceError)
} }
} }
func latestBlockHeight() async throws -> BlockHeight { 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<ZcashCompactBlock, Error> { func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
@ -176,11 +194,15 @@ extension LightWalletGRPCService: LightWalletService {
} }
} }
} }
func submit(spendTransaction: Data) async throws -> LightWalletServiceResponse { func submit(spendTransaction: Data) async throws -> LightWalletServiceResponse {
do { do {
let transaction = RawTransaction.with { $0.data = spendTransaction } if let torConn {
return try await compactTxStreamer.sendTransaction(transaction) return try torConn.submit(spendTransaction: spendTransaction)
} else {
let transaction = RawTransaction.with { $0.data = spendTransaction }
return try await compactTxStreamer.sendTransaction(transaction)
}
} catch { } catch {
let serviceError = error.mapToServiceError() let serviceError = error.mapToServiceError()
throw ZcashError.serviceSubmitFailed(serviceError) throw ZcashError.serviceSubmitFailed(serviceError)
@ -188,37 +210,41 @@ extension LightWalletGRPCService: LightWalletService {
} }
func fetchTransaction(txId: Data) async throws -> (tx: ZcashTransaction.Fetched?, status: TransactionStatus) { func fetchTransaction(txId: Data) async throws -> (tx: ZcashTransaction.Fetched?, status: TransactionStatus) {
var txFilter = TxFilter() if let torConn {
txFilter.hash = txId return try torConn.fetchTransaction(txId: txId)
} else {
do { var txFilter = TxFilter()
let rawTx = try await compactTxStreamer.getTransaction(txFilter) txFilter.hash = txId
let isNotMined = rawTx.height == 0 || rawTx.height > UInt32.max do {
let rawTx = try await compactTxStreamer.getTransaction(txFilter)
return (
tx: let isNotMined = rawTx.height == 0 || rawTx.height > UInt32.max
ZcashTransaction.Fetched(
rawID: txId, return (
minedHeight: isNotMined ? nil : UInt32(rawTx.height), tx:
raw: rawTx.data ZcashTransaction.Fetched(
), rawID: txId,
status: isNotMined ? .notInMainChain : .mined(Int(rawTx.height)) minedHeight: isNotMined ? nil : UInt32(rawTx.height),
) raw: rawTx.data
} catch let error as GRPCStatus { ),
if error.makeGRPCStatus().code == .notFound { status: isNotMined ? .notInMainChain : .mined(Int(rawTx.height))
return (tx: nil, .txidNotRecognized) )
} else if let notFound = error.message?.contains("Transaction not found"), notFound { } catch let error as GRPCStatus {
return (tx: nil, .txidNotRecognized) if error.makeGRPCStatus().code == .notFound {
} else if let notFound = error.message?.contains("No such mempool or blockchain transaction. Use gettransaction for wallet transactions."), notFound { return (tx: nil, .txidNotRecognized)
return (tx: nil, .txidNotRecognized) } else if let notFound = error.message?.contains("Transaction not found"), notFound {
} else { 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() let serviceError = error.mapToServiceError()
throw ZcashError.serviceFetchTransactionFailed(serviceError) 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 { 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<RawTransaction, Error> { func getTaddressTxids(_ request: TransparentAddressBlockFilter) -> AsyncThrowingStream<RawTransaction, Error> {

View File

@ -136,9 +136,10 @@ protocol LightWalletServiceResponse {
struct LightWalletServiceFactory { struct LightWalletServiceFactory {
let endpoint: LightWalletEndpoint let endpoint: LightWalletEndpoint
let torURL: URL?
func make() -> LightWalletService { func make() -> LightWalletService {
return LightWalletGRPCService(endpoint: endpoint) return LightWalletGRPCService(endpoint: endpoint, torURL: torURL)
} }
} }

View File

@ -101,7 +101,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
) )
guard let ffiAddressPtr else { 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) } defer { zcashlc_free_ffi_address(ffiAddressPtr) }
@ -126,7 +127,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
guard let boxedSlice = boxedSlicePtr?.pointee else { guard let boxedSlice = boxedSlicePtr?.pointee else {
throw ZcashError.rustDeriveUnifiedSpendingKey( throw ZcashError.rustDeriveUnifiedSpendingKey(
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveUnifiedSpendingKey` failed with unknown error")) ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveUnifiedSpendingKey` failed with unknown error")
)
} }
return boxedSlice.unsafeToUnifiedSpendingKey(network: networkType) return boxedSlice.unsafeToUnifiedSpendingKey(network: networkType)
@ -207,7 +209,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
guard let key = boxedSlicePtr?.pointee else { guard let key = boxedSlicePtr?.pointee else {
throw ZcashError.rustDeriveArbitraryWalletKey( throw ZcashError.rustDeriveArbitraryWalletKey(
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryWalletKey` failed with unknown error")) ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryWalletKey` failed with unknown error")
)
} }
return key.ptr.toByteArray( return key.ptr.toByteArray(
@ -237,7 +240,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
guard let key = boxedSlicePtr?.pointee else { guard let key = boxedSlicePtr?.pointee else {
throw ZcashError.rustDeriveArbitraryAccountKey( throw ZcashError.rustDeriveArbitraryAccountKey(
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryAccountKey` failed with unknown error")) ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryAccountKey` failed with unknown error")
)
} }
return key.ptr.toByteArray( return key.ptr.toByteArray(

View File

@ -76,7 +76,7 @@ enum Dependencies {
} }
container.register(type: LightWalletService.self, isSingleton: true) { _ in 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 container.register(type: TransactionRepository.self, isSingleton: true) { _ in

View File

@ -180,8 +180,8 @@ public class SDKSynchronizer: Synchronizer {
var areFundsSpendable = false var areFundsSpendable = false
if let scanProgress = walletSummary?.scanProgress { if let scanProgress = walletSummary?.scanProgress {
let composedNumerator: Float = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0) let composedNumerator = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0)
let composedDenominator: Float = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0) let composedDenominator = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0)
let progress: Float let progress: Float
if composedDenominator == 0 { if composedDenominator == 0 {
@ -749,7 +749,8 @@ public class SDKSynchronizer: Synchronizer {
port: $0.port, port: $0.port,
secure: $0.secure, secure: $0.secure,
singleCallTimeout: 5000, singleCallTimeout: 5000,
streamingCallTimeout: Int64(fetchThresholdSeconds) * 1000 streamingCallTimeout: Int64(fetchThresholdSeconds) * 1000,
torURL: initializer.torDirURL
), ),
url: "\($0.host):\($0.port)" 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 // 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 // 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( LightWalletGRPCService(
host: endpoint.host, host: endpoint.host,
port: endpoint.port, port: endpoint.port,
secure: endpoint.secure, secure: endpoint.secure,
singleCallTimeout: 5000, singleCallTimeout: 5000,
streamingCallTimeout: endpoint.streamingCallTimeoutInMillis streamingCallTimeout: endpoint.streamingCallTimeoutInMillis,
torURL: torURL
) )
} }
@ -934,8 +936,8 @@ public class SDKSynchronizer: Synchronizer {
// SWITCH TO NEW ENDPOINT // SWITCH TO NEW ENDPOINT
// LightWalletService dependency update // LightWalletService dependency update
initializer.container.register(type: LightWalletService.self, isSingleton: true) { _ in initializer.container.register(type: LightWalletService.self, isSingleton: true) { [torURL = initializer.torDirURL] _ in
LightWalletGRPCService(endpoint: endpoint) LightWalletGRPCService(endpoint: endpoint, torURL: torURL)
} }
// DEPENDENCIES // DEPENDENCIES

View File

@ -12,7 +12,7 @@ public class TorClient {
private let runtime: OpaquePointer private let runtime: OpaquePointer
public var cachedFiatCurrencyResult: FiatCurrencyResult? public var cachedFiatCurrencyResult: FiatCurrencyResult?
init(torDir: URL) async throws { init(torDir: URL) throws {
// Ensure that the directory exists. // Ensure that the directory exists.
let fileManager = FileManager() let fileManager = FileManager()
if !fileManager.fileExists(atPath: torDir.path) { if !fileManager.fileExists(atPath: torDir.path) {
@ -41,20 +41,19 @@ public class TorClient {
zcashlc_free_tor_runtime(runtime) zcashlc_free_tor_runtime(runtime)
} }
public func isolatedClient() async throws -> TorClient { public func isolatedClient() throws -> TorClient {
let isolatedPtr = zcashlc_tor_isolated_client(runtime) let isolatedPtr = zcashlc_tor_isolated_client(runtime)
guard let isolatedPtr else { guard let isolatedPtr else {
throw ZcashError.rustTorIsolatedClient( throw ZcashError.rustTorIsolatedClient(
lastErrorMessage( lastErrorMessage(fallback: "`TorClient.isolatedClient` failed with unknown error")
fallback: )
"`TorClient.isolatedClient` failed with unknown error"))
} }
return TorClient(runtimePtr: isolatedPtr) return TorClient(runtimePtr: isolatedPtr)
} }
public func getExchangeRateUSD() async throws -> FiatCurrencyResult { public func getExchangeRateUSD() throws -> FiatCurrencyResult {
let rate = zcashlc_get_exchange_rate_usd(runtime) let rate = zcashlc_get_exchange_rate_usd(runtime)
if rate.is_sign_negative { if rate.is_sign_negative {
@ -64,8 +63,10 @@ public class TorClient {
let newValue = FiatCurrencyResult( let newValue = FiatCurrencyResult(
date: Date(), date: Date(),
rate: NSDecimalNumber( rate: NSDecimalNumber(
mantissa: rate.mantissa, exponent: rate.exponent, mantissa: rate.mantissa,
isNegative: rate.is_sign_negative), exponent: rate.exponent,
isNegative: rate.is_sign_negative
),
state: .success state: .success
) )
@ -74,23 +75,19 @@ public class TorClient {
return newValue return newValue
} }
public func connectToLightwalletd(endpoint: String) async throws public func connectToLightwalletd(endpoint: String) throws -> TorLwdConn {
-> TorLwdConn
{
guard !endpoint.containsCStringNullBytesBeforeStringEnding() else { guard !endpoint.containsCStringNullBytesBeforeStringEnding() else {
throw ZcashError.rustTorConnectToLightwalletd( throw ZcashError.rustTorConnectToLightwalletd("endpoint string contains null bytes")
"endpoint string contains null bytes")
} }
let lwdConnPtr = zcashlc_tor_connect_to_lightwalletd( let lwdConnPtr = zcashlc_tor_connect_to_lightwalletd(
runtime, [CChar](endpoint.utf8CString)) runtime, [CChar](endpoint.utf8CString)
)
guard let lwdConnPtr else { guard let lwdConnPtr else {
throw ZcashError.rustTorConnectToLightwalletd( throw ZcashError.rustTorConnectToLightwalletd(
lastErrorMessage( lastErrorMessage(fallback: "`TorClient.connectToLightwalletd` failed with unknown error")
fallback: )
"`TorClient.connectToLightwalletd` failed with unknown error"
))
} }
return TorLwdConn(connPtr: lwdConnPtr) return TorLwdConn(connPtr: lwdConnPtr)
@ -100,7 +97,7 @@ public class TorClient {
public class TorLwdConn { public class TorLwdConn {
private let conn: OpaquePointer private let conn: OpaquePointer
fileprivate init(connPtr: OpaquePointer) { init(connPtr: OpaquePointer) {
conn = connPtr conn = connPtr
} }
@ -111,9 +108,7 @@ public class TorLwdConn {
/// Submits a raw transaction over lightwalletd. /// Submits a raw transaction over lightwalletd.
/// - Parameter spendTransaction: data representing the transaction to be sent /// - Parameter spendTransaction: data representing the transaction to be sent
/// - Throws: `serviceSubmitFailed` when GRPC call fails. /// - Throws: `serviceSubmitFailed` when GRPC call fails.
func submit(spendTransaction: Data) async throws func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
-> LightWalletServiceResponse
{
let success = zcashlc_tor_lwd_conn_submit_transaction( let success = zcashlc_tor_lwd_conn_submit_transaction(
conn, conn,
spendTransaction.bytes, spendTransaction.bytes,
@ -121,18 +116,21 @@ public class TorLwdConn {
) )
var response = SendResponse() var response = SendResponse()
if !success { if !success {
let err = lastErrorMessage( let err = lastErrorMessage(fallback: "`TorLwdConn.submit` failed with unknown error")
fallback: "`TorLwdConn.submit` failed with unknown error")
if err.hasPrefix("Failed to submit transaction (") if err.hasPrefix("Failed to submit transaction (") && err.contains(")") {
&& err.contains(")") guard let startOfCode = err.firstIndex(of: "(") else {
{ throw ZcashError.rustTorLwdSubmit(err)
let startOfCode = err.firstIndex(of: "(")! }
let endOfCode = err.firstIndex(of: ")")! guard let endOfCode = err.firstIndex(of: ")") else {
let errorCode = Int32( throw ZcashError.rustTorLwdSubmit(err)
err[err.index(startOfCode, offsetBy: 1)..<endOfCode])! }
let errorMessage = String( guard let errorCode = Int32(err[err.index(startOfCode, offsetBy: 1)..<endOfCode]) else {
err[err.index(endOfCode, offsetBy: 3)...]) throw ZcashError.rustTorLwdSubmit(err)
}
let errorMessage = String(err[err.index(endOfCode, offsetBy: 3)...])
response.errorCode = errorCode response.errorCode = errorCode
response.errorMessage = errorMessage response.errorMessage = errorMessage
@ -140,6 +138,7 @@ public class TorLwdConn {
throw ZcashError.rustTorLwdSubmit(err) throw ZcashError.rustTorLwdSubmit(err)
} }
} }
return response return response
} }
@ -148,9 +147,7 @@ public class TorLwdConn {
/// - Throws: LightWalletServiceError /// - Throws: LightWalletServiceError
/// - Returns: LightWalletServiceResponse /// - Returns: LightWalletServiceResponse
/// - Throws: `serviceFetchTransactionFailed` when GRPC call fails. /// - Throws: `serviceFetchTransactionFailed` when GRPC call fails.
func fetchTransaction(txId: Data) async throws -> ( func fetchTransaction(txId: Data) throws -> (tx: ZcashTransaction.Fetched?, status: TransactionStatus) {
tx: ZcashTransaction.Fetched?, status: TransactionStatus
) {
guard txId.count == 32 else { guard txId.count == 32 else {
throw ZcashError.rustGetMemoInvalidTxIdLength throw ZcashError.rustGetMemoInvalidTxIdLength
} }
@ -161,10 +158,7 @@ public class TorLwdConn {
guard let txPtr else { guard let txPtr else {
throw ZcashError.rustTorLwdFetchTransaction( throw ZcashError.rustTorLwdFetchTransaction(
lastErrorMessage( lastErrorMessage(fallback: "`TorLwdConn.fetchTransaction` failed with unknown error")
fallback:
"`TorLwdConn.fetchTransaction` failed with unknown error"
)
) )
} }
@ -185,4 +179,81 @@ public class TorLwdConn {
status: isNotMined ? .notInMainChain : .mined(Int(height)) 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<UInt8>(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<UInt8>(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)")
}
}
} }

View File

@ -27,7 +27,7 @@ class BlockDownloaderTests: XCTestCase {
try await super.setUp() try await super.setUp()
testTempDirectory = Environment.uniqueTestTempDirectory testTempDirectory = Environment.uniqueTestTempDirectory
service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make() service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default, torURL: nil).make()
rustBackend = ZcashRustBackend.makeForTests( rustBackend = ZcashRustBackend.makeForTests(
fsBlockDbRoot: testTempDirectory, fsBlockDbRoot: testTempDirectory,

View File

@ -97,7 +97,7 @@ class TransactionEnhancementTests: ZcashTestCase {
return return
} }
let service = DarksideWalletService() let service = DarksideWalletService(torURL: nil)
darksideWalletService = service darksideWalletService = service
let storage = FSCompactBlockRepository( let storage = FSCompactBlockRepository(

View File

@ -52,7 +52,7 @@ class BlockStreamingTest: ZcashTestCase {
singleCallTimeoutInMillis: 10000, singleCallTimeoutInMillis: 10000,
streamingCallTimeoutInMillis: 10000 streamingCallTimeoutInMillis: 10000
) )
let service = LightWalletServiceFactory(endpoint: endpoint).make() let service = LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make()
latestBlockHeight = try await service.latestBlockHeight() latestBlockHeight = try await service.latestBlockHeight()
startHeight = latestBlockHeight - 10_000 startHeight = latestBlockHeight - 10_000
@ -77,7 +77,7 @@ class BlockStreamingTest: ZcashTestCase {
streamingCallTimeoutInMillis: timeout streamingCallTimeoutInMillis: timeout
) )
self.endpoint = endpoint self.endpoint = endpoint
service = LightWalletServiceFactory(endpoint: endpoint).make() service = LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make()
storage = FSCompactBlockRepository( storage = FSCompactBlockRepository(
fsBlockDbRoot: testTempDirectory, fsBlockDbRoot: testTempDirectory,
metadataStore: FSMetadataStore.live( metadataStore: FSMetadataStore.live(
@ -97,7 +97,7 @@ class BlockStreamingTest: ZcashTestCase {
) )
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in
LightWalletServiceFactory(endpoint: endpoint).make() LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make()
} }
let transactionRepositoryMock = TransactionRepositoryMock() let transactionRepositoryMock = TransactionRepositoryMock()

View File

@ -43,7 +43,7 @@ class CompactBlockProcessorTests: ZcashTestCase {
network: ZcashNetworkBuilder.network(for: .testnet) network: ZcashNetworkBuilder.network(for: .testnet)
) )
let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make()
let service = MockLightWalletService( let service = MockLightWalletService(
latestBlockHeight: mockLatestHeight, latestBlockHeight: mockLatestHeight,
service: liveService service: liveService

View File

@ -44,7 +44,7 @@ class CompactBlockReorgTests: ZcashTestCase {
network: ZcashNetworkBuilder.network(for: .testnet) network: ZcashNetworkBuilder.network(for: .testnet)
) )
let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make()
let service = MockLightWalletService( let service = MockLightWalletService(
latestBlockHeight: mockLatestHeight, latestBlockHeight: mockLatestHeight,
service: liveService service: liveService

View File

@ -46,7 +46,7 @@ class DownloadTests: ZcashTestCase {
ZcashRustBackend.makeForTests(fsBlockDbRoot: self.testTempDirectory, networkType: self.network.networkType) ZcashRustBackend.makeForTests(fsBlockDbRoot: self.testTempDirectory, networkType: self.network.networkType)
} }
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in 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) let storage = mockContainer.resolve(CompactBlockRepository.self)
try await storage.create() try await storage.create()

View File

@ -19,7 +19,7 @@ class LightWalletServiceTests: XCTestCase {
override func setUp() { override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class. // Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp() super.setUp()
service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make()
} }
override func tearDownWithError() throws { override func tearDownWithError() throws {

View File

@ -14,23 +14,23 @@ import XCTest
class TorClientTests: ZcashTestCase { class TorClientTests: ZcashTestCase {
let network: ZcashNetwork = ZcashNetworkBuilder.network(for: .testnet) let network: ZcashNetwork = ZcashNetworkBuilder.network(for: .testnet)
func testLwdCanFetchAndSubmitTx() async throws { func testLwdCanFetchAndSubmitTx() throws {
// Spin up a new Tor client. // 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. // Connect to a testnet lightwalletd server.
let lwdConn = try await client.connectToLightwalletd( let lwdConn = try client.connectToLightwalletd(
endpoint: LightWalletEndpointBuilder.publicTestnet.urlString) endpoint: LightWalletEndpointBuilder.publicTestnet.urlString)
// Fetch a known testnet transaction. // Fetch a known testnet transaction.
let txId = let txId =
"9e309d29a99f06e6dcc7aee91dca23c0efc2cf5083cc483463ddbee19c1fadf1" "9e309d29a99f06e6dcc7aee91dca23c0efc2cf5083cc483463ddbee19c1fadf1"
.toTxIdString().hexadecimal! .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)) XCTAssertEqual(status, .mined(1_234_567))
// We should fail to resubmit the already-mined transaction. // 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.errorCode, -25)
XCTAssertEqual( XCTAssertEqual(
result.errorMessage, result.errorMessage,

View File

@ -50,9 +50,9 @@ class DarksideWalletService: LightWalletService {
var service: LightWalletService var service: LightWalletService
var darksideService: DarksideStreamerNIOClient var darksideService: DarksideStreamerNIOClient
init(endpoint: LightWalletEndpoint) { init(endpoint: LightWalletEndpoint, torURL: URL?) {
self.channel = ChannelProvider().channel(endpoint: endpoint) 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) self.darksideService = DarksideStreamerNIOClient(channel: channel)
} }
@ -62,8 +62,8 @@ class DarksideWalletService: LightWalletService {
self.service = service self.service = service
} }
convenience init() { convenience init(torURL: URL?) {
self.init(endpoint: LightWalletEndpointBuilder.default) self.init(endpoint: LightWalletEndpointBuilder.default, torURL: torURL)
} }
func blockStream(startHeight: BlockHeight, endHeight: BlockHeight) -> AsyncThrowingStream<ZcashCompactBlock, Error> { func blockStream(startHeight: BlockHeight, endHeight: BlockHeight) -> AsyncThrowingStream<ZcashCompactBlock, Error> {

View File

@ -93,7 +93,7 @@ class TestCoordinator {
self.birthday = walletBirthday self.birthday = walletBirthday
self.network = network 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.service = DarksideWalletService(endpoint: endpoint, service: liveService)
self.synchronizer = SDKSynchronizer(initializer: initializer) self.synchronizer = SDKSynchronizer(initializer: initializer)
subscribeToState(synchronizer: self.synchronizer) subscribeToState(synchronizer: self.synchronizer)