diff --git a/CHANGELOG.md b/CHANGELOG.md index afa5dad36..14aade1b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased +## Added +- `Synchronizer.exchangeRateUSDStream: AnyPublisher`, + which returns the currently-cached USD/ZEC exchange rate, or `nil` if it has not yet been + fetched. +- `Synchronizer.refreshExchangeRateUSD()`, which refreshes the rate returned by + `Synchronizer.exchangeRateUSDStream`. Prices are queried over Tor (to hide the wallet's + IP address). + # 2.1.12 - 2024-07-04 ## Fixed diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8ab51ea9c..8195c5b18 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -176,8 +176,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "f16fbed56fb3ba4f2cc53ead344a2eca77fa5aae", - "version" : "0.8.1" + "revision" : "4de1b42f99aebfc5e4f0340da8a66a4f719db9a6" } } ], diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift index c477a43d5..a07bd91bd 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift @@ -48,12 +48,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { fsBlockDbRoot: try! fsBlockDbRootURLHelper(), generalStorageURL: try! generalStorageURLHelper(), dataDbURL: try! dataDbURLHelper(), + torDirURL: try! torDirURLHelper(), endpoint: DemoAppConfig.endpoint, network: kZcashNetwork, spendParamsURL: try! spendParamsURLHelper(), outputParamsURL: try! outputParamsURLHelper(), - saplingParamsSourceURL: SaplingParamsSourceURL.default, - enableBackendTracing: true + saplingParamsSourceURL: SaplingParamsSourceURL.default ) self.wallet = wallet @@ -199,6 +199,15 @@ func dataDbURLHelper() throws -> URL { ) } +func torDirURLHelper() throws -> URL { + try documentsDirectoryHelper() + .appendingPathComponent(kZcashNetwork.networkType.chainName) + .appendingPathComponent( + ZcashSDK.defaultTorDirName, + isDirectory: true + ) +} + func spendParamsURLHelper() throws -> URL { try documentsDirectoryHelper().appendingPathComponent("sapling-spend.params") } diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift index ccee9d6aa..89fe2cf96 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift @@ -8,21 +8,49 @@ import UIKit import ZcashLightClientKit +import Combine class GetBalanceViewController: UIViewController { @IBOutlet weak var balance: UILabel! @IBOutlet weak var verified: UILabel! + var cancellable: AnyCancellable? + + var accountBalance: AccountBalance? + var rate: FiatCurrencyResult? + override func viewDidLoad() { super.viewDidLoad() let synchronizer = AppDelegate.shared.sharedSynchronizer self.title = "Account 0 Balance" - Task { @MainActor in - let balanceText = (try? await synchronizer.getAccountBalance()?.saplingBalance.total().formattedString) ?? "0.0" - let verifiedText = (try? await synchronizer.getAccountBalance()?.saplingBalance.spendableValue.formattedString) ?? "0.0" - self.balance.text = "\(balanceText) ZEC" - self.verified.text = "\(verifiedText) ZEC" + Task { @MainActor [weak self] in + self?.accountBalance = try? await synchronizer.getAccountBalance() + self?.updateLabels() + } + + cancellable = synchronizer.exchangeRateUSDStream.sink { [weak self] result in + self?.rate = result + self?.updateLabels() + } + + synchronizer.refreshExchangeRateUSD() + } + + func updateLabels() { + DispatchQueue.main.async { [weak self] in + let balanceText = (self?.accountBalance?.saplingBalance.total().formattedString) ?? "0.0" + let verifiedText = (self?.accountBalance?.saplingBalance.spendableValue.formattedString) ?? "0.0" + + if let usdZecRate = self?.rate { + let usdBalance = (self?.accountBalance?.saplingBalance.total().decimalValue ?? 0).multiplying(by: usdZecRate.rate) + let usdVerified = (self?.accountBalance?.saplingBalance.spendableValue.decimalValue ?? 0).multiplying(by: usdZecRate.rate) + self?.balance.text = "\(balanceText) ZEC\n\(usdBalance) USD\n\n(\(usdZecRate.rate) USD/ZEC)" + self?.verified.text = "\(verifiedText) ZEC\n\(usdVerified) USD" + } else { + self?.balance.text = "\(balanceText) ZEC" + self?.verified.text = "\(verifiedText) ZEC" + } } } } diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift index 1904de453..acddf9ff4 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift @@ -102,14 +102,14 @@ class SyncBlocksListViewController: UIViewController { fsBlockDbRoot: try! fsBlockDbRootURLHelper(), generalStorageURL: try! generalStorageURLHelper(), dataDbURL: try! dataDbURLHelper(), + torDirURL: try! torDirURLHelper(), endpoint: DemoAppConfig.endpoint, network: kZcashNetwork, spendParamsURL: try! spendParamsURLHelper(), outputParamsURL: try! outputParamsURLHelper(), saplingParamsSourceURL: SaplingParamsSourceURL.default, alias: data.alias, - loggingPolicy: .default(.debug), - enableBackendTracing: true + loggingPolicy: .default(.debug) ) return SDKSynchronizer(initializer: initializer) diff --git a/Package.resolved b/Package.resolved index c8615efd9..da0595336 100644 --- a/Package.resolved +++ b/Package.resolved @@ -122,8 +122,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "9314c83d7a09d88e1c0bd3ff3738a50833325059", - "version" : "0.8.0" + "revision" : "4de1b42f99aebfc5e4f0340da8a66a4f719db9a6" } } ], diff --git a/Package.swift b/Package.swift index 2c92df608..9f5145d83 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,9 @@ let package = Package( dependencies: [ .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.19.1"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"), - .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", exact: "0.8.1") + // .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", exact: "0.8.1") + // Compiled from 2516a94f8bdc540d951c38b66e9c07e2b8c29cb4 + .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "4de1b42f99aebfc5e4f0340da8a66a4f719db9a6") ], targets: [ .target( diff --git a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift index e3e4d0fa5..4f49de00e 100644 --- a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift +++ b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift @@ -56,6 +56,7 @@ actor CompactBlockProcessor { let saplingParamsSourceURL: SaplingParamsSourceURL let fsBlockCacheRoot: URL let dataDb: URL + let torDir: URL let spendParamsURL: URL let outputParamsURL: URL let enhanceBatchSize: Int @@ -79,6 +80,7 @@ actor CompactBlockProcessor { cacheDbURL: URL? = nil, fsBlockCacheRoot: URL, dataDb: URL, + torDir: URL, spendParamsURL: URL, outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, @@ -94,6 +96,7 @@ actor CompactBlockProcessor { self.alias = alias self.fsBlockCacheRoot = fsBlockCacheRoot self.dataDb = dataDb + self.torDir = torDir self.spendParamsURL = spendParamsURL self.outputParamsURL = outputParamsURL self.saplingParamsSourceURL = saplingParamsSourceURL @@ -112,6 +115,7 @@ actor CompactBlockProcessor { alias: ZcashSynchronizerAlias, fsBlockCacheRoot: URL, dataDb: URL, + torDir: URL, spendParamsURL: URL, outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, @@ -126,6 +130,7 @@ actor CompactBlockProcessor { self.alias = alias self.fsBlockCacheRoot = fsBlockCacheRoot self.dataDb = dataDb + self.torDir = torDir self.spendParamsURL = spendParamsURL self.outputParamsURL = outputParamsURL self.saplingParamsSourceURL = saplingParamsSourceURL @@ -151,6 +156,7 @@ actor CompactBlockProcessor { alias: initializer.alias, fsBlockCacheRoot: initializer.fsBlockDbRoot, dataDb: initializer.dataDbURL, + torDir: initializer.torDirURL, spendParamsURL: initializer.spendParamsURL, outputParamsURL: initializer.outputParamsURL, saplingParamsSourceURL: initializer.saplingParamsSourceURL, diff --git a/Sources/ZcashLightClientKit/ClosureSynchronizer.swift b/Sources/ZcashLightClientKit/ClosureSynchronizer.swift index 18d9b1ea2..00d2583a5 100644 --- a/Sources/ZcashLightClientKit/ClosureSynchronizer.swift +++ b/Sources/ZcashLightClientKit/ClosureSynchronizer.swift @@ -128,7 +128,9 @@ public protocol ClosureSynchronizer { func refreshUTXOs(address: TransparentAddress, from height: BlockHeight, completion: @escaping (Result) -> Void) func getAccountBalance(accountIndex: Int, completion: @escaping (Result) -> Void) - + + func refreshExchangeRateUSD() + /* It can be missleading that these two methods are returning Publisher even this protocol is closure based. Reason is that Synchronizer doesn't provide different implementations for these two methods. So Combine it is even here. diff --git a/Sources/ZcashLightClientKit/CombineSynchronizer.swift b/Sources/ZcashLightClientKit/CombineSynchronizer.swift index 09aa08421..295791985 100644 --- a/Sources/ZcashLightClientKit/CombineSynchronizer.swift +++ b/Sources/ZcashLightClientKit/CombineSynchronizer.swift @@ -129,7 +129,7 @@ public protocol CombineSynchronizer { func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) -> SinglePublisher - func getAccountBalance(accountIndex: Int) -> SinglePublisher + func refreshExchangeRateUSD() func rewind(_ policy: RewindPolicy) -> CompletablePublisher func wipe() -> CompletablePublisher diff --git a/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift b/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift index 22770e883..208e61cc7 100644 --- a/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift +++ b/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift @@ -124,6 +124,9 @@ public enum ZcashSDK { /// Default Name for LibRustZcash data.db public static let defaultDataDbName = "data.db" + /// Default Name for Tor data directory + public static let defaultTorDirName = "tor" + /// Default Name for Compact Block file system based db public static let defaultFsCacheName = "fs_cache" @@ -154,6 +157,9 @@ public protocol NetworkConstants { /// Default Name for LibRustZcash data.db static var defaultDataDbName: String { get } + /// Default Name for Tor data directory + static var defaultTorDirName: String { get } + static var defaultFsBlockDbRootName: String { get } /// Default Name for Compact Block caches db @@ -181,6 +187,9 @@ public enum ZcashSDKMainnetConstants: NetworkConstants { /// Default Name for LibRustZcash data.db public static let defaultDataDbName = "data.db" + /// Default Name for Tor data directory + public static let defaultTorDirName = "tor" + public static let defaultFsBlockDbRootName = "fs_cache" /// Default Name for Compact Block caches db @@ -197,6 +206,9 @@ public enum ZcashSDKTestnetConstants: NetworkConstants { /// Default Name for LibRustZcash data.db public static let defaultDataDbName = "data.db" + /// Default Name for Tor data directory + public static let defaultTorDirName = "tor" + /// Default Name for Compact Block caches db public static let defaultCacheDbName = "caches.db" diff --git a/Sources/ZcashLightClientKit/Error/Sourcery/generateErrorCode.sh b/Sources/ZcashLightClientKit/Error/Sourcery/generateErrorCode.sh index b240aea18..a74b10b5f 100755 --- a/Sources/ZcashLightClientKit/Error/Sourcery/generateErrorCode.sh +++ b/Sources/ZcashLightClientKit/Error/Sourcery/generateErrorCode.sh @@ -3,11 +3,11 @@ scriptDir=${0:a:h} cd "${scriptDir}" -sourcery_version=2.1.7 +sourcery_version=2.2.5 if which sourcery >/dev/null; then if [[ $(sourcery --version) != $sourcery_version ]]; then - echo "warning: Compatible sourcer version not installed. Install sourcer $sourcery_version. Currently installed version is $(sourcer --version)" + echo "warning: Compatible sourcery version not installed. Install sourcery $sourcery_version. Currently installed version is $(sourcery --version)" exit 1 fi diff --git a/Sources/ZcashLightClientKit/Error/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index ebea735c7..d07054fba 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.2.5 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* @@ -342,6 +342,16 @@ public enum ZcashError: Equatable, Error { /// sourcery: code="ZRUST0061" /// ZRUST0061 case rustPutOrchardSubtreeRoots(_ rustError: String) + /// Error from rust layer when calling TorClient.init + /// - `rustError` contains error generated by the rust layer. + /// sourcery: code="ZRUST0062" + /// ZRUST0062 + case rustTorClientInit(_ rustError: String) + /// Error from rust layer when calling TorClient.get + /// - `rustError` contains error generated by the rust layer. + /// sourcery: code="ZRUST0063" + /// ZRUST0063 + case rustTorClientGet(_ rustError: String) /// SQLite query failed when fetching all accounts from the database. /// - `sqliteError` is error produced by SQLite library. /// ZADAO0001 @@ -509,6 +519,9 @@ public enum ZcashError: Equatable, Error { /// Can't create `Recipient` because input is invalid. /// ZWLTP0007 case recipientInvalidInput + /// Can't create `TexAddress` because input is invalid. + /// ZWLTP0008 + case texAddressInvalidInput /// WalletTransactionEncoder wants to create transaction but files with sapling parameters are not present on disk. /// ZWLTE0001 case walletTransEncoderCreateTransactionMissingSaplingParams @@ -705,6 +718,8 @@ public enum ZcashError: Equatable, Error { case .rustIsSeedRelevantToAnyDerivedAccount: return "Error from rust layer when calling ZcashRustBackend.rustIsSeedRelevantToAnyDerivedAccount" case .rustPutOrchardSubtreeRootsAllocationProblem: return "Unable to allocate memory required to write blocks when calling ZcashRustBackend.putOrchardSubtreeRoots" case .rustPutOrchardSubtreeRoots: return "Error from rust layer when calling ZcashRustBackend.putOrchardSubtreeRoots" + case .rustTorClientInit: return "Error from rust layer when calling TorClient.init" + case .rustTorClientGet: return "Error from rust layer when calling TorClient.get" 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." @@ -757,6 +772,7 @@ public enum ZcashError: Equatable, Error { case .saplingAddressInvalidInput: return "Can't create `SaplingAddress` because input is invalid." case .unifiedAddressInvalidInput: return "Can't create `UnifiedAddress` because input is invalid." case .recipientInvalidInput: return "Can't create `Recipient` because input is invalid." + case .texAddressInvalidInput: return "Can't create `TexAddress` because input is invalid." case .walletTransEncoderCreateTransactionMissingSaplingParams: return "WalletTransactionEncoder wants to create transaction but files with sapling parameters are not present on disk." case .walletTransEncoderShieldFundsMissingSaplingParams: return "WalletTransactionEncoder wants to shield funds but files with sapling parameters are not present on disk." case .zatoshiDecode: return "Initiatilzation fo `Zatoshi` from a decoder failed." @@ -885,6 +901,8 @@ public enum ZcashError: Equatable, Error { case .rustIsSeedRelevantToAnyDerivedAccount: return .rustIsSeedRelevantToAnyDerivedAccount case .rustPutOrchardSubtreeRootsAllocationProblem: return .rustPutOrchardSubtreeRootsAllocationProblem case .rustPutOrchardSubtreeRoots: return .rustPutOrchardSubtreeRoots + case .rustTorClientInit: return .rustTorClientInit + case .rustTorClientGet: return .rustTorClientGet case .accountDAOGetAll: return .accountDAOGetAll case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode case .accountDAOFindBy: return .accountDAOFindBy @@ -937,6 +955,7 @@ public enum ZcashError: Equatable, Error { case .saplingAddressInvalidInput: return .saplingAddressInvalidInput case .unifiedAddressInvalidInput: return .unifiedAddressInvalidInput case .recipientInvalidInput: return .recipientInvalidInput + case .texAddressInvalidInput: return .texAddressInvalidInput case .walletTransEncoderCreateTransactionMissingSaplingParams: return .walletTransEncoderCreateTransactionMissingSaplingParams case .walletTransEncoderShieldFundsMissingSaplingParams: return .walletTransEncoderShieldFundsMissingSaplingParams case .zatoshiDecode: return .zatoshiDecode diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift index be9510df4..f5b905276 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.2.5 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* @@ -185,6 +185,10 @@ public enum ZcashErrorCode: String { case rustPutOrchardSubtreeRootsAllocationProblem = "ZRUST0060" /// Error from rust layer when calling ZcashRustBackend.putOrchardSubtreeRoots case rustPutOrchardSubtreeRoots = "ZRUST0061" + /// Error from rust layer when calling TorClient.init + case rustTorClientInit = "ZRUST0062" + /// Error from rust layer when calling TorClient.get + case rustTorClientGet = "ZRUST0063" /// SQLite query failed when fetching all accounts from the database. case accountDAOGetAll = "ZADAO0001" /// Fetched accounts from SQLite but can't decode them. @@ -289,6 +293,8 @@ public enum ZcashErrorCode: String { case unifiedAddressInvalidInput = "ZWLTP0006" /// Can't create `Recipient` because input is invalid. case recipientInvalidInput = "ZWLTP0007" + /// Can't create `TexAddress` because input is invalid. + case texAddressInvalidInput = "ZWLTP0008" /// WalletTransactionEncoder wants to create transaction but files with sapling parameters are not present on disk. case walletTransEncoderCreateTransactionMissingSaplingParams = "ZWLTE0001" /// WalletTransactionEncoder wants to shield funds but files with sapling parameters are not present on disk. diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift index c85c97b46..927e52739 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift @@ -367,6 +367,14 @@ enum ZcashErrorDefinition { /// - `rustError` contains error generated by the rust layer. /// sourcery: code="ZRUST0061" case rustPutOrchardSubtreeRoots(_ rustError: String) + /// Error from rust layer when calling TorClient.init + /// - `rustError` contains error generated by the rust layer. + /// sourcery: code="ZRUST0062" + case rustTorClientInit(_ rustError: String) + /// Error from rust layer when calling TorClient.get + /// - `rustError` contains error generated by the rust layer. + /// sourcery: code="ZRUST0063" + case rustTorClientGet(_ rustError: String) // MARK: - Account DAO @@ -571,6 +579,9 @@ enum ZcashErrorDefinition { /// Can't create `Recipient` because input is invalid. // sourcery: code="ZWLTP0007" case recipientInvalidInput + /// Can't create `TexAddress` because input is invalid. + // sourcery: code="ZWLTP0008" + case texAddressInvalidInput // MARK: - WalletTransactionEncoder diff --git a/Sources/ZcashLightClientKit/Initializer.swift b/Sources/ZcashLightClientKit/Initializer.swift index 51b62f453..30772870e 100644 --- a/Sources/ZcashLightClientKit/Initializer.swift +++ b/Sources/ZcashLightClientKit/Initializer.swift @@ -90,6 +90,7 @@ public class Initializer { struct URLs { let fsBlockDbRoot: URL let dataDbURL: URL + let torDirURL: URL let generalStorageURL: URL let spendParamsURL: URL let outputParamsURL: URL @@ -115,6 +116,7 @@ public class Initializer { let fsBlockDbRoot: URL let generalStorageURL: URL let dataDbURL: URL + let torDirURL: URL let spendParamsURL: URL let outputParamsURL: URL let saplingParamsSourceURL: SaplingParamsSourceURL @@ -158,14 +160,14 @@ public class Initializer { fsBlockDbRoot: URL, generalStorageURL: URL, dataDbURL: URL, + torDirURL: URL, endpoint: LightWalletEndpoint, network: ZcashNetwork, spendParamsURL: URL, outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, alias: ZcashSynchronizerAlias = .default, - loggingPolicy: LoggingPolicy = .default(.debug), - enableBackendTracing: Bool = false + loggingPolicy: LoggingPolicy = .default(.debug) ) { let container = DIContainer() @@ -177,14 +179,14 @@ public class Initializer { fsBlockDbRoot: fsBlockDbRoot, generalStorageURL: generalStorageURL, dataDbURL: dataDbURL, + torDirURL: torDirURL, endpoint: endpoint, network: network, spendParamsURL: spendParamsURL, outputParamsURL: outputParamsURL, saplingParamsSourceURL: saplingParamsSourceURL, alias: alias, - loggingPolicy: loggingPolicy, - enableBackendTracing: enableBackendTracing + loggingPolicy: loggingPolicy ) self.init( @@ -207,14 +209,14 @@ public class Initializer { fsBlockDbRoot: URL, generalStorageURL: URL, dataDbURL: URL, + torDirURL: URL, endpoint: LightWalletEndpoint, network: ZcashNetwork, spendParamsURL: URL, outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, alias: ZcashSynchronizerAlias = .default, - loggingPolicy: LoggingPolicy = .default(.debug), - enableBackendTracing: Bool = false + loggingPolicy: LoggingPolicy = .default(.debug) ) { // It's not possible to fail from constructor. Technically it's possible but it can be pain for the client apps to handle errors thrown // from constructor. So `parsingError` is just stored in initializer and `SDKSynchronizer.prepare()` throw this error if it exists. @@ -224,14 +226,14 @@ public class Initializer { fsBlockDbRoot: fsBlockDbRoot, generalStorageURL: generalStorageURL, dataDbURL: dataDbURL, + torDirURL: torDirURL, endpoint: endpoint, network: network, spendParamsURL: spendParamsURL, outputParamsURL: outputParamsURL, saplingParamsSourceURL: saplingParamsSourceURL, alias: alias, - loggingPolicy: loggingPolicy, - enableBackendTracing: enableBackendTracing + loggingPolicy: loggingPolicy ) self.init( @@ -264,6 +266,7 @@ public class Initializer { self.fsBlockDbRoot = urls.fsBlockDbRoot self.generalStorageURL = urls.generalStorageURL self.dataDbURL = urls.dataDbURL + self.torDirURL = urls.torDirURL self.endpoint = endpoint self.spendParamsURL = urls.spendParamsURL self.outputParamsURL = urls.outputParamsURL @@ -286,18 +289,19 @@ public class Initializer { fsBlockDbRoot: URL, generalStorageURL: URL, dataDbURL: URL, + torDirURL: URL, endpoint: LightWalletEndpoint, network: ZcashNetwork, spendParamsURL: URL, outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, alias: ZcashSynchronizerAlias, - loggingPolicy: LoggingPolicy = .default(.debug), - enableBackendTracing: Bool = false + loggingPolicy: LoggingPolicy = .default(.debug) ) -> (URLs, ZcashError?) { let urls = URLs( fsBlockDbRoot: fsBlockDbRoot, dataDbURL: dataDbURL, + torDirURL: torDirURL, generalStorageURL: generalStorageURL, spendParamsURL: spendParamsURL, outputParamsURL: outputParamsURL @@ -313,8 +317,7 @@ public class Initializer { alias: alias, networkType: network.networkType, endpoint: endpoint, - loggingPolicy: loggingPolicy, - enableBackendTracing: enableBackendTracing + loggingPolicy: loggingPolicy ) return (updatedURLs, parsingError) @@ -362,6 +365,10 @@ public class Initializer { return .failure(.initializerCantUpdateURLWithAlias(urls.dataDbURL)) } + guard let updatedTorDirURL = urls.torDirURL.updateLastPathComponent(with: alias) else { + return .failure(.initializerCantUpdateURLWithAlias(urls.torDirURL)) + } + guard let updatedSpendParamsURL = urls.spendParamsURL.updateLastPathComponent(with: alias) else { return .failure(.initializerCantUpdateURLWithAlias(urls.spendParamsURL)) } @@ -378,6 +385,7 @@ public class Initializer { URLs( fsBlockDbRoot: updatedFsBlockDbRoot, dataDbURL: updatedDataDbURL, + torDirURL: updatedTorDirURL, generalStorageURL: updatedGeneralStorageURL, spendParamsURL: updatedSpendParamsURL, outputParamsURL: updateOutputParamsURL diff --git a/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift b/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift new file mode 100644 index 000000000..350dd344c --- /dev/null +++ b/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift @@ -0,0 +1,25 @@ +// +// FiatCurrencyResult.swift +// +// +// Created by Lukáš Korba on 31.07.2024. +// + +import Foundation + +/// The model representing currency for ZEC-XXX conversion. Initial implementation +/// provides only USD value. +public struct FiatCurrencyResult: Equatable { + public enum State: Equatable { + /// Last fetch failed, cached value is returned instead. + case error + /// Refresh has been triggered, returning cached value but informing about request in flight. + case fetching + /// Fetch of the value ended up as success so new value in returned. + case success + } + + public let date: Date + public let rate: NSDecimalNumber + public var state: State +} diff --git a/Sources/ZcashLightClientKit/Model/WalletTypes.swift b/Sources/ZcashLightClientKit/Model/WalletTypes.swift index 6a6fec13e..2313049ed 100644 --- a/Sources/ZcashLightClientKit/Model/WalletTypes.swift +++ b/Sources/ZcashLightClientKit/Model/WalletTypes.swift @@ -89,13 +89,15 @@ public enum AddressType: Equatable { case p2sh case sapling case unified - + case tex + var id: UInt32 { switch self { case .p2pkh: return 0 case .p2sh: return 1 case .sapling: return 2 case .unified: return 3 + case .tex: return 4 } } } @@ -107,6 +109,7 @@ extension AddressType { case 1: return .p2sh case 2: return .sapling case 3: return .unified + case 4: return .tex default: return nil } } @@ -213,6 +216,35 @@ public struct UnifiedAddress: Equatable, StringEncoded { } } +/// A transparent-source-only (TEX) Address that can be encoded as a String +/// +/// Transactions sent to this address are totally visible in the public +/// ledger. See "Multiple transaction types" in https://z.cash/technology/ +/// +/// Transactions sent to this address must only have transparent inputs. See ZIP 320: https://zips.z.cash/zip-0320 +public struct TexAddress: Equatable, StringEncoded, Comparable { + let encoding: String + + public var stringEncoded: String { encoding } + + /// Initializes a new TexAddress from the provided string encoding + /// + /// - parameter encoding: String encoding of the TEX address + /// - parameter network: `NetworkType` corresponding to the encoding (Mainnet or Testnet) + /// - Throws: `texAddressInvalidInput`when the provided encoding is found to be invalid + public init(encoding: String, network: NetworkType) throws { + guard DerivationTool(networkType: network).isValidTexAddress(encoding) else { + throw ZcashError.texAddressInvalidInput + } + + self.encoding = encoding + } + + public static func < (lhs: TexAddress, rhs: TexAddress) -> Bool { + return lhs.encoding < rhs.encoding + } +} + public enum TransactionRecipient: Equatable { case address(Recipient) case internalAccount(UInt32) @@ -223,6 +255,7 @@ public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) + case tex(TexAddress) public var stringEncoded: String { switch self { @@ -232,6 +265,8 @@ public enum Recipient: Equatable, StringEncoded { return zAddr.stringEncoded case .unified(let uAddr): return uAddr.stringEncoded + case .tex(let texAddr): + return texAddr.stringEncoded } } @@ -246,6 +281,8 @@ public enum Recipient: Equatable, StringEncoded { self = .sapling(sapling) } else if let transparent = try? TransparentAddress(encoding: string, network: network) { self = .transparent(transparent) + } else if let tex = try? TexAddress(encoding: string, network: network) { + self = .tex(tex) } else { throw ZcashError.recipientInvalidInput } @@ -259,6 +296,7 @@ public enum Recipient: Equatable, StringEncoded { case .p2sh: return (.transparent(TransparentAddress(validatedEncoding: encoded)), metadata.networkType) case .sapling: return (.sapling(SaplingAddress(validatedEncoding: encoded)), metadata.networkType) case .unified: return (.unified(UnifiedAddress(validatedEncoding: encoded, networkType: metadata.networkType)), metadata.networkType) + case .tex: return (.tex(TexAddress(validatedEncoding: encoded)), metadata.networkType) } } } diff --git a/Sources/ZcashLightClientKit/Providers/ResourceProvider.swift b/Sources/ZcashLightClientKit/Providers/ResourceProvider.swift index a19441b95..e9ffcfd04 100644 --- a/Sources/ZcashLightClientKit/Providers/ResourceProvider.swift +++ b/Sources/ZcashLightClientKit/Providers/ResourceProvider.swift @@ -31,6 +31,16 @@ public struct DefaultResourceProvider: ResourceProvider { } } + public var torDirURL: URL { + let constants = network.constants + do { + let url = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + return url.appendingPathComponent(constants.defaultTorDirName) + } catch { + return URL(fileURLWithPath: "file://\(constants.defaultTorDirName)") + } + } + public var fsCacheURL: URL { let constants = network.constants do { diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 6a0034f46..c01e4492b 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -9,6 +9,21 @@ import Foundation import libzcashlc +enum RustLogging: String { + /// The logs are completely disabled. + case off + /// Logs very serious errors. + case error + /// Logs hazardous situations. + case warn + /// Logs useful information. + case info + /// Logs lower priority information. + case debug + /// Logs very low priority, often extremely verbose, information. + case trace +} + struct ZcashRustBackend: ZcashRustBackendWelding { let minimumConfirmations: UInt32 = 10 let minimumShieldingConfirmations: UInt32 = 1 @@ -22,7 +37,8 @@ struct ZcashRustBackend: ZcashRustBackendWelding { let networkType: NetworkType - static var tracingEnabled = false + static var rustInitialized = false + /// Creates instance of `ZcashRustBackend`. /// - Parameters: /// - dbData: `URL` pointing to file where data database will be. @@ -32,9 +48,17 @@ struct ZcashRustBackend: ZcashRustBackendWelding { /// - spendParamsPath: `URL` pointing to spend parameters file. /// - outputParamsPath: `URL` pointing to output parameters file. /// - networkType: Network type to use. - /// - enableTracing: this sets up whether the tracing system will dump logs onto the OSLogger system or not. - /// **Important note:** this will enable the tracing **for all instances** of ZcashRustBackend, not only for this one. - init(dbData: URL, fsBlockDbRoot: URL, spendParamsPath: URL, outputParamsPath: URL, networkType: NetworkType, enableTracing: Bool = false) { + /// - logLevel: this sets up whether the tracing system will dump logs onto the OSLogger system or not. + /// **Important note:** this will enable the tracing **for all instances** of ZcashRustBackend, not only for this one. + /// This is ignored after the first ZcashRustBackend instance is created. + init( + dbData: URL, + fsBlockDbRoot: URL, + spendParamsPath: URL, + outputParamsPath: URL, + networkType: NetworkType, + logLevel: RustLogging = RustLogging.off + ) { self.dbData = dbData.osStr() self.fsBlockDbRoot = fsBlockDbRoot.osPathStr() self.spendParamsPath = spendParamsPath.osPathStr() @@ -42,9 +66,9 @@ struct ZcashRustBackend: ZcashRustBackendWelding { self.networkType = networkType self.keyDeriving = ZcashKeyDerivationBackend(networkType: networkType) - if enableTracing && !Self.tracingEnabled { - Self.tracingEnabled = true - Self.enableTracing() + if !Self.rustInitialized { + Self.rustInitialized = true + Self.initializeRust(logLevel: logLevel) } } @@ -823,33 +847,33 @@ struct ZcashRustBackend: ZcashRustBackendWelding { } private extension ZcashRustBackend { - static func enableTracing() { - zcashlc_init_on_load(false) + static func initializeRust(logLevel: RustLogging) { + logLevel.rawValue.utf8CString.withUnsafeBufferPointer { levelPtr in + zcashlc_init_on_load(levelPtr.baseAddress) + } } } -private extension ZcashRustBackend { - nonisolated func lastErrorMessage(fallback: String) -> String { - let errorLen = zcashlc_last_error_length() - defer { zcashlc_clear_last_error() } - - if errorLen > 0 { - let error = UnsafeMutablePointer.allocate(capacity: Int(errorLen)) - defer { error.deallocate() } - - zcashlc_error_message_utf8(error, errorLen) - if let errorMessage = String(validatingUTF8: error) { - return errorMessage - } else { - return fallback - } +nonisolated func lastErrorMessage(fallback: String) -> String { + let errorLen = zcashlc_last_error_length() + defer { zcashlc_clear_last_error() } + + if errorLen > 0 { + let error = UnsafeMutablePointer.allocate(capacity: Int(errorLen)) + defer { error.deallocate() } + + zcashlc_error_message_utf8(error, errorLen) + if let errorMessage = String(validatingUTF8: error) { + return errorMessage } else { return fallback } + } else { + return fallback } } -private extension URL { +extension URL { func osStr() -> (String, UInt) { let path = self.absoluteString return (path, UInt(path.lengthOfBytes(using: .utf8))) diff --git a/Sources/ZcashLightClientKit/Synchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer.swift index 34057680d..c09ee5808 100644 --- a/Sources/ZcashLightClientKit/Synchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer.swift @@ -101,6 +101,9 @@ public protocol Synchronizer: AnyObject { /// This stream is backed by `PassthroughSubject`. Check `SynchronizerEvent` to see which events may be emitted. var eventStream: AnyPublisher { get } + /// This stream emits the latest known USD/ZEC exchange rate, paired with the time it was queried. See `FiatCurrencyResult`. + var exchangeRateUSDStream: AnyPublisher { get } + /// Initialize the wallet. The ZIP-32 seed bytes can optionally be passed to perform /// database migrations. most of the times the seed won't be needed. If they do and are /// not provided this will fail with `InitializationResult.seedRequired`. It could @@ -309,6 +312,9 @@ public protocol Synchronizer: AnyObject { /// - Returns: `AccountBalance`, struct that holds sapling and unshielded balances or `nil` when no account is associated with `accountIndex` func getAccountBalance(accountIndex: Int) async throws -> AccountBalance? + /// Fetches the latest ZEC-USD exchange rate and updates `exchangeRateUSDSubject`. + func refreshExchangeRateUSD() + /// Rescans the known blocks with the current keys. /// /// `rewind(policy:)` can be called anytime. If the sync process is in progress then it is stopped first. In this case, it make some significant diff --git a/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift index 1f2d31ae5..5c54ac69d 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift @@ -194,6 +194,10 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer { } } + public func refreshExchangeRateUSD() { + synchronizer.refreshExchangeRateUSD() + } + /* It can be missleading that these two methods are returning Publisher even this protocol is closure based. Reason is that Synchronizer doesn't provide different implementations for these two methods. So Combine it is even here. diff --git a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift index 1c8a1a910..fd910bf84 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift @@ -196,6 +196,10 @@ extension CombineSDKSynchronizer: CombineSynchronizer { } } + public func refreshExchangeRateUSD() { + synchronizer.refreshExchangeRateUSD() + } + public func rewind(_ policy: RewindPolicy) -> CompletablePublisher { synchronizer.rewind(policy) } public func wipe() -> CompletablePublisher { synchronizer.wipe() } } diff --git a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift index 91bd561c2..a4d3920e7 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift @@ -14,8 +14,7 @@ enum Dependencies { alias: ZcashSynchronizerAlias, networkType: NetworkType, endpoint: LightWalletEndpoint, - loggingPolicy: Initializer.LoggingPolicy = .default(.debug), - enableBackendTracing: Bool = false + loggingPolicy: Initializer.LoggingPolicy = .default(.debug) ) { container.register(type: CheckpointSource.self, isSingleton: true) { _ in CheckpointSourceFactory.fromBundle(for: networkType) @@ -35,6 +34,36 @@ enum Dependencies { return logger } + let rustLogging: RustLogging + switch loggingPolicy { + case .default(let logLevel): + switch logLevel { + case .debug: + rustLogging = RustLogging.debug + case .info, .event: + rustLogging = RustLogging.info + case .warning: + rustLogging = RustLogging.warn + case .error: + rustLogging = RustLogging.error + } + case .custom(let logger): + switch logger.maxLogLevel() { + case .debug: + rustLogging = RustLogging.debug + case .info, .event: + rustLogging = RustLogging.info + case .warning: + rustLogging = RustLogging.warn + case .error: + rustLogging = RustLogging.error + case .none: + rustLogging = RustLogging.off + } + case .noLogging: + rustLogging = RustLogging.off + } + container.register(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in ZcashRustBackend( dbData: urls.dataDbURL, @@ -42,7 +71,7 @@ enum Dependencies { spendParamsPath: urls.spendParamsURL, outputParamsPath: urls.outputParamsURL, networkType: networkType, - enableTracing: enableBackendTracing + logLevel: rustLogging ) } diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index 671c2d605..1f03a2f93 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -22,8 +22,12 @@ public class SDKSynchronizer: Synchronizer { private let eventSubject = PassthroughSubject() public var eventStream: AnyPublisher { eventSubject.eraseToAnyPublisher() } + private let exchangeRateUSDSubject = CurrentValueSubject(nil) + public var exchangeRateUSDStream: AnyPublisher { exchangeRateUSDSubject.eraseToAnyPublisher() } + let metrics: SDKMetrics public let logger: Logger + var tor: TorClient? // Don't read this variable directly. Use `status` instead. And don't update this variable directly use `updateStatus()` methods instead. private var underlyingStatus: GenericActor @@ -82,12 +86,14 @@ public class SDKSynchronizer: Synchronizer { self.syncSession = SyncSession(.nullID) self.syncSessionTicker = syncSessionTicker self.latestBlocksDataProvider = initializer.container.resolve(LatestBlocksDataProvider.self) - + initializer.lightWalletService.connectionStateChange = { [weak self] oldState, newState in self?.connectivityStateChanged(oldState: oldState, newState: newState) } - Task(priority: .high) { [weak self] in await self?.subscribeToProcessorEvents(blockProcessor) } + Task(priority: .high) { [weak self] in + await self?.subscribeToProcessorEvents(blockProcessor) + } } deinit { @@ -508,6 +514,41 @@ public class SDKSynchronizer: Synchronizer { try await initializer.rustBackend.getWalletSummary()?.accountBalances[UInt32(accountIndex)] } + /// Fetches the latest ZEC-USD exchange rate. + public func refreshExchangeRateUSD() { + // ignore refresh request when one is already in flight + if let latestState = tor?.cachedFiatCurrencyResult?.state, latestState == .fetching { + return + } + + // broadcast cached value but update the state + if let cachedFiatCurrencyResult = tor?.cachedFiatCurrencyResult { + var fetchingState = cachedFiatCurrencyResult + fetchingState.state = .fetching + tor?.cachedFiatCurrencyResult = fetchingState + + exchangeRateUSDSubject.send(fetchingState) + } + + Task { + do { + if tor == nil { + logger.info("Bootstrapping Tor client for fetching exchange rates") + tor = try await TorClient(torDir: initializer.torDirURL) + } + // broadcast new value in case of success + exchangeRateUSDSubject.send(try await tor?.getExchangeRateUSD()) + } catch { + // broadcast cached value but update the state + var errorState = tor?.cachedFiatCurrencyResult + errorState?.state = .error + tor?.cachedFiatCurrencyResult = errorState + + exchangeRateUSDSubject.send(errorState) + } + } + } + public func getUnifiedAddress(accountIndex: Int) async throws -> UnifiedAddress { try await blockProcessor.getUnifiedAddress(accountIndex: accountIndex) } diff --git a/Sources/ZcashLightClientKit/Tool/DerivationTool.swift b/Sources/ZcashLightClientKit/Tool/DerivationTool.swift index 848d3553e..542361691 100644 --- a/Sources/ZcashLightClientKit/Tool/DerivationTool.swift +++ b/Sources/ZcashLightClientKit/Tool/DerivationTool.swift @@ -127,6 +127,12 @@ extension DerivationTool: KeyValidation { } ?? false } + public func isValidTexAddress(_ texAddress: String) -> Bool { + DerivationTool.getAddressMetadata(texAddress).map { + $0.networkType == backend.networkType && $0.addressType == AddressType.tex + } ?? false + } + public func isValidSaplingExtendedSpendingKey(_ extsk: String) -> Bool { backend.isValidSaplingExtendedSpendingKey(extsk) } @@ -163,6 +169,16 @@ extension UnifiedAddress { } } +extension TexAddress { + /// This constructor is for internal use for Strings encodings that are assumed to be + /// already validated by another function. only for internal use. Unless you are + /// constructing an address from a primitive function of the FFI, you probably + /// shouldn't be using this. + init(validatedEncoding: String) { + self.encoding = validatedEncoding + } +} + extension UnifiedFullViewingKey { /// This constructor is for internal use for Strings encodings that are assumed to be /// already validated by another function. only for internal use. Unless you are diff --git a/Sources/ZcashLightClientKit/Tor/TorClient.swift b/Sources/ZcashLightClientKit/Tor/TorClient.swift new file mode 100644 index 000000000..2c3707606 --- /dev/null +++ b/Sources/ZcashLightClientKit/Tor/TorClient.swift @@ -0,0 +1,57 @@ +// +// TorRuntime.swift +// +// +// Created by Jack Grigg on 04/06/2024. +// + +import Foundation +import libzcashlc + +public class TorClient { + private let runtime: OpaquePointer + public var cachedFiatCurrencyResult: FiatCurrencyResult? + + init(torDir: URL) async throws { + // Ensure that the directory exists. + let fileManager = FileManager() + if !fileManager.fileExists(atPath: torDir.path) { + do { + try fileManager.createDirectory(at: torDir, withIntermediateDirectories: true) + } catch { + throw ZcashError.blockRepositoryCreateBlocksCacheDirectory(torDir, error) + } + } + + let rawDir = torDir.osPathStr() + let runtimePtr = zcashlc_create_tor_runtime(rawDir.0, rawDir.1) + + guard let runtimePtr else { + throw ZcashError.rustTorClientInit(lastErrorMessage(fallback: "`TorClient` init failed with unknown error")) + } + + runtime = runtimePtr + } + + deinit { + zcashlc_free_tor_runtime(runtime) + } + + public func getExchangeRateUSD() async throws -> FiatCurrencyResult { + let rate = zcashlc_get_exchange_rate_usd(runtime) + + if rate.is_sign_negative { + throw ZcashError.rustTorClientGet(lastErrorMessage(fallback: "`TorClient.get` failed with unknown error")) + } + + let newValue = FiatCurrencyResult( + date: Date(), + rate: NSDecimalNumber(mantissa: rate.mantissa, exponent: rate.exponent, isNegative: rate.is_sign_negative), + state: .success + ) + + cachedFiatCurrencyResult = newValue + + return newValue + } +} diff --git a/Sources/ZcashLightClientKit/Utils/LoggingProxy.swift b/Sources/ZcashLightClientKit/Utils/LoggingProxy.swift index f8b1d56b5..0d6f151fb 100644 --- a/Sources/ZcashLightClientKit/Utils/LoggingProxy.swift +++ b/Sources/ZcashLightClientKit/Utils/LoggingProxy.swift @@ -11,6 +11,7 @@ import Foundation Represents what's expected from a logging entity */ public protocol Logger { + func maxLogLevel() -> OSLogger.LogLevel? func debug(_ message: String, file: StaticString, function: StaticString, line: Int) func info(_ message: String, file: StaticString, function: StaticString, line: Int) func event(_ message: String, file: StaticString, function: StaticString, line: Int) @@ -44,6 +45,9 @@ extension Logger { A concrete logger implementation that logs nothing at all */ struct NullLogger: Logger { + func maxLogLevel() -> OSLogger.LogLevel? { + nil + } func debug(_ message: String, file: StaticString, function: StaticString, line: Int) {} func info(_ message: String, file: StaticString, function: StaticString, line: Int) {} func event(_ message: String, file: StaticString, function: StaticString, line: Int) {} diff --git a/Sources/ZcashLightClientKit/Utils/OSLogger.swift b/Sources/ZcashLightClientKit/Utils/OSLogger.swift index e19a03d6c..373f67cbc 100644 --- a/Sources/ZcashLightClientKit/Utils/OSLogger.swift +++ b/Sources/ZcashLightClientKit/Utils/OSLogger.swift @@ -39,6 +39,10 @@ public class OSLogger: Logger { } } + public func maxLogLevel() -> LogLevel? { + self.level + } + public func debug( _ message: String, file: StaticString = #file, diff --git a/Tests/DarksideTests/TransactionEnhancementTests.swift b/Tests/DarksideTests/TransactionEnhancementTests.swift index 52ff65ed2..7ef9b85b2 100644 --- a/Tests/DarksideTests/TransactionEnhancementTests.swift +++ b/Tests/DarksideTests/TransactionEnhancementTests.swift @@ -60,6 +60,7 @@ class TransactionEnhancementTests: ZcashTestCase { alias: .default, fsBlockCacheRoot: testTempDirectory, dataDb: pathProvider.dataDbURL, + torDir: pathProvider.torDirURL, spendParamsURL: pathProvider.spendParamsURL, outputParamsURL: pathProvider.outputParamsURL, saplingParamsSourceURL: SaplingParamsSourceURL.tests, @@ -117,6 +118,7 @@ class TransactionEnhancementTests: ZcashTestCase { urls: Initializer.URLs( fsBlockDbRoot: testTempDirectory, dataDbURL: pathProvider.dataDbURL, + torDirURL: pathProvider.torDirURL, generalStorageURL: testGeneralStorageDirectory, spendParamsURL: pathProvider.spendParamsURL, outputParamsURL: pathProvider.outputParamsURL diff --git a/Tests/NetworkTests/BlockStreamingTest.swift b/Tests/NetworkTests/BlockStreamingTest.swift index 41504a041..54398a081 100644 --- a/Tests/NetworkTests/BlockStreamingTest.swift +++ b/Tests/NetworkTests/BlockStreamingTest.swift @@ -31,6 +31,7 @@ class BlockStreamingTest: ZcashTestCase { urls: Initializer.URLs( fsBlockDbRoot: testTempDirectory, dataDbURL: try! __dataDbURL(), + torDirURL: try! __torDirURL(), generalStorageURL: testGeneralStorageDirectory, spendParamsURL: try! __spendParamsURL(), outputParamsURL: try! __outputParamsURL() diff --git a/Tests/NetworkTests/CompactBlockProcessorTests.swift b/Tests/NetworkTests/CompactBlockProcessorTests.swift index 3f550f839..06cec3190 100644 --- a/Tests/NetworkTests/CompactBlockProcessorTests.swift +++ b/Tests/NetworkTests/CompactBlockProcessorTests.swift @@ -35,6 +35,7 @@ class CompactBlockProcessorTests: ZcashTestCase { alias: .default, fsBlockCacheRoot: testTempDirectory, dataDb: pathProvider.dataDbURL, + torDir: pathProvider.torDirURL, spendParamsURL: pathProvider.spendParamsURL, outputParamsURL: pathProvider.outputParamsURL, saplingParamsSourceURL: SaplingParamsSourceURL.tests, @@ -71,6 +72,7 @@ class CompactBlockProcessorTests: ZcashTestCase { urls: Initializer.URLs( fsBlockDbRoot: testTempDirectory, dataDbURL: processorConfig.dataDb, + torDirURL: processorConfig.torDir, generalStorageURL: testGeneralStorageDirectory, spendParamsURL: processorConfig.spendParamsURL, outputParamsURL: processorConfig.outputParamsURL diff --git a/Tests/NetworkTests/CompactBlockReorgTests.swift b/Tests/NetworkTests/CompactBlockReorgTests.swift index 12b1e2ff1..b4b13e518 100644 --- a/Tests/NetworkTests/CompactBlockReorgTests.swift +++ b/Tests/NetworkTests/CompactBlockReorgTests.swift @@ -36,6 +36,7 @@ class CompactBlockReorgTests: ZcashTestCase { alias: .default, fsBlockCacheRoot: testTempDirectory, dataDb: pathProvider.dataDbURL, + torDir: pathProvider.torDirURL, spendParamsURL: pathProvider.spendParamsURL, outputParamsURL: pathProvider.outputParamsURL, saplingParamsSourceURL: SaplingParamsSourceURL.tests, @@ -94,6 +95,7 @@ class CompactBlockReorgTests: ZcashTestCase { urls: Initializer.URLs( fsBlockDbRoot: testTempDirectory, dataDbURL: processorConfig.dataDb, + torDirURL: processorConfig.torDir, generalStorageURL: testGeneralStorageDirectory, spendParamsURL: processorConfig.spendParamsURL, outputParamsURL: processorConfig.outputParamsURL diff --git a/Tests/NetworkTests/DownloadTests.swift b/Tests/NetworkTests/DownloadTests.swift index 66b468590..56a7450d4 100644 --- a/Tests/NetworkTests/DownloadTests.swift +++ b/Tests/NetworkTests/DownloadTests.swift @@ -23,6 +23,7 @@ class DownloadTests: ZcashTestCase { urls: Initializer.URLs( fsBlockDbRoot: testTempDirectory, dataDbURL: try! __dataDbURL(), + torDirURL: try! __torDirURL(), generalStorageURL: testGeneralStorageDirectory, spendParamsURL: try! __spendParamsURL(), outputParamsURL: try! __outputParamsURL() diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift index 127e589a1..ef9026058 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift @@ -235,6 +235,7 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { cacheDbURL: underlyingCacheDbURL ?? defaultConfig.cacheDbURL, fsBlockCacheRoot: underlyingFsBlockCacheRoot ?? defaultConfig.fsBlockCacheRoot, dataDb: defaultConfig.dataDb, + torDir: defaultConfig.torDir, spendParamsURL: defaultConfig.spendParamsURL, outputParamsURL: defaultConfig.outputParamsURL, saplingParamsSourceURL: defaultConfig.saplingParamsSourceURL, diff --git a/Tests/OfflineTests/InitializerOfflineTests.swift b/Tests/OfflineTests/InitializerOfflineTests.swift index 1c781f294..80b1419a0 100644 --- a/Tests/OfflineTests/InitializerOfflineTests.swift +++ b/Tests/OfflineTests/InitializerOfflineTests.swift @@ -20,6 +20,7 @@ class InitializerOfflineTests: XCTestCase { private func makeInitializer( fsBlockDbRoot: URL, dataDbURL: URL, + torDirURL: URL, generalStorageURL: URL, spendParamsURL: URL, outputParamsURL: URL, @@ -30,6 +31,7 @@ class InitializerOfflineTests: XCTestCase { fsBlockDbRoot: fsBlockDbRoot, generalStorageURL: generalStorageURL, dataDbURL: dataDbURL, + torDirURL: torDirURL, endpoint: LightWalletEndpointBuilder.default, network: ZcashNetworkBuilder.network(for: .testnet), spendParamsURL: spendParamsURL, @@ -54,6 +56,7 @@ class InitializerOfflineTests: XCTestCase { private func genericTestForURLsParsingFailures( fsBlockDbRoot: URL, dataDbURL: URL, + torDirURL: URL, generalStorageURL: URL, spendParamsURL: URL, outputParamsURL: URL, @@ -63,6 +66,7 @@ class InitializerOfflineTests: XCTestCase { let initializer = makeInitializer( fsBlockDbRoot: fsBlockDbRoot, dataDbURL: dataDbURL, + torDirURL: torDirURL, generalStorageURL: generalStorageURL, spendParamsURL: spendParamsURL, outputParamsURL: outputParamsURL, @@ -85,6 +89,7 @@ class InitializerOfflineTests: XCTestCase { let initializer = makeInitializer( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, @@ -102,6 +107,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: invalidPathURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, @@ -113,6 +119,19 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: invalidPathURL, + torDirURL: validDirectoryURL, + generalStorageURL: validDirectoryURL, + spendParamsURL: validFileURL, + outputParamsURL: validFileURL, + alias: .default + ) + } + + func test__defaultAlias__invalidTorDirURL__errorIsGenerated() { + genericTestForURLsParsingFailures( + fsBlockDbRoot: validDirectoryURL, + dataDbURL: validFileURL, + torDirURL: invalidPathURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, @@ -124,6 +143,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: invalidPathURL, outputParamsURL: validFileURL, @@ -135,6 +155,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: invalidPathURL, @@ -146,6 +167,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: invalidPathURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, @@ -158,6 +180,7 @@ class InitializerOfflineTests: XCTestCase { let initializer = makeInitializer( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, @@ -175,6 +198,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: invalidPathURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, @@ -186,6 +210,19 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: invalidPathURL, + torDirURL: validDirectoryURL, + generalStorageURL: validDirectoryURL, + spendParamsURL: validFileURL, + outputParamsURL: validFileURL, + alias: .custom("alias") + ) + } + + func test__customAlias__invalidTorDirURL__errorIsGenerated() { + genericTestForURLsParsingFailures( + fsBlockDbRoot: validDirectoryURL, + dataDbURL: validFileURL, + torDirURL: invalidPathURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, @@ -197,6 +234,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: invalidPathURL, outputParamsURL: validFileURL, @@ -208,6 +246,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: validDirectoryURL, spendParamsURL: validFileURL, outputParamsURL: invalidPathURL, @@ -219,6 +258,7 @@ class InitializerOfflineTests: XCTestCase { genericTestForURLsParsingFailures( fsBlockDbRoot: validDirectoryURL, dataDbURL: validFileURL, + torDirURL: validDirectoryURL, generalStorageURL: invalidPathURL, spendParamsURL: validFileURL, outputParamsURL: validFileURL, diff --git a/Tests/OfflineTests/SynchronizerOfflineTests.swift b/Tests/OfflineTests/SynchronizerOfflineTests.swift index 3d701a14b..181e14889 100644 --- a/Tests/OfflineTests/SynchronizerOfflineTests.swift +++ b/Tests/OfflineTests/SynchronizerOfflineTests.swift @@ -314,6 +314,7 @@ class SynchronizerOfflineTests: ZcashTestCase { fsBlockDbRoot: validDirectoryURL, generalStorageURL: validDirectoryURL, dataDbURL: invalidPathURL, + torDirURL: validDirectoryURL, endpoint: LightWalletEndpointBuilder.default, network: ZcashNetworkBuilder.network(for: .testnet), spendParamsURL: validFileURL, @@ -349,6 +350,7 @@ class SynchronizerOfflineTests: ZcashTestCase { fsBlockDbRoot: validDirectoryURL, generalStorageURL: validDirectoryURL, dataDbURL: invalidPathURL, + torDirURL: validDirectoryURL, endpoint: LightWalletEndpointBuilder.default, network: ZcashNetworkBuilder.network(for: .testnet), spendParamsURL: validFileURL, diff --git a/Tests/OfflineTests/WalletTests.swift b/Tests/OfflineTests/WalletTests.swift index ea6321fc4..ce057baec 100644 --- a/Tests/OfflineTests/WalletTests.swift +++ b/Tests/OfflineTests/WalletTests.swift @@ -45,6 +45,7 @@ class WalletTests: ZcashTestCase { fsBlockDbRoot: testTempDirectory, generalStorageURL: testGeneralStorageDirectory, dataDbURL: try __dataDbURL(), + torDirURL: try __torDirURL(), endpoint: LightWalletEndpointBuilder.default, network: network, spendParamsURL: try __spendParamsURL(), diff --git a/Tests/PerformanceTests/SynchronizerTests.swift b/Tests/PerformanceTests/SynchronizerTests.swift index 6ca368dbc..9d526b65a 100644 --- a/Tests/PerformanceTests/SynchronizerTests.swift +++ b/Tests/PerformanceTests/SynchronizerTests.swift @@ -64,6 +64,7 @@ class SynchronizerTests: ZcashTestCase { fsBlockDbRoot: databases.fsCacheDbRoot, generalStorageURL: testGeneralStorageDirectory, dataDbURL: databases.dataDB, + torDirURL: databases.torDir, endpoint: endpoint, network: network, spendParamsURL: try __spendParamsURL(), diff --git a/Tests/TestUtils/DarkSideWalletService.swift b/Tests/TestUtils/DarkSideWalletService.swift index 386801381..a9412b251 100644 --- a/Tests/TestUtils/DarkSideWalletService.swift +++ b/Tests/TestUtils/DarkSideWalletService.swift @@ -214,7 +214,11 @@ enum DarksideWalletDConstants: NetworkConstants { static var defaultDataDbName: String { ZcashSDKMainnetConstants.defaultDataDbName } - + + static var defaultTorDirName: String { + ZcashSDKMainnetConstants.defaultTorDirName + } + static var defaultCacheDbName: String { ZcashSDKMainnetConstants.defaultCacheDbName } diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 7d2798e3f..9a7856e6a 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 2.2.4 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.2.5 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import Combine @testable import ZcashLightClientKit @@ -1072,6 +1072,24 @@ class LoggerMock: Logger { ) { } + // MARK: - maxLogLevel + + var maxLogLevelCallsCount = 0 + var maxLogLevelCalled: Bool { + return maxLogLevelCallsCount > 0 + } + var maxLogLevelReturnValue: OSLogger.LogLevel? + var maxLogLevelClosure: (() -> OSLogger.LogLevel?)? + + func maxLogLevel() -> OSLogger.LogLevel? { + maxLogLevelCallsCount += 1 + if let closure = maxLogLevelClosure { + return closure() + } else { + return maxLogLevelReturnValue + } + } + // MARK: - debug var debugFileFunctionLineCallsCount = 0 @@ -1293,6 +1311,10 @@ class SynchronizerMock: Synchronizer { get { return underlyingEventStream } } var underlyingEventStream: AnyPublisher! + var exchangeRateUSDStream: AnyPublisher { + get { return underlyingExchangeRateUSDStream } + } + var underlyingExchangeRateUSDStream: AnyPublisher! var transactions: [ZcashTransaction.Overview] { get async { return underlyingTransactions } } @@ -1780,6 +1802,19 @@ class SynchronizerMock: Synchronizer { } } + // MARK: - refreshExchangeRateUSD + + var refreshExchangeRateUSDCallsCount = 0 + var refreshExchangeRateUSDCalled: Bool { + return refreshExchangeRateUSDCallsCount > 0 + } + var refreshExchangeRateUSDClosure: (() -> Void)? + + func refreshExchangeRateUSD() { + refreshExchangeRateUSDCallsCount += 1 + refreshExchangeRateUSDClosure!() + } + // MARK: - rewind var rewindCallsCount = 0 diff --git a/Tests/TestUtils/Sourcery/generateMocks.sh b/Tests/TestUtils/Sourcery/generateMocks.sh index 70ed0b1ff..7f1916948 100755 --- a/Tests/TestUtils/Sourcery/generateMocks.sh +++ b/Tests/TestUtils/Sourcery/generateMocks.sh @@ -3,7 +3,7 @@ scriptDir=${0:a:h} cd "${scriptDir}" -sourcery_version=2.2.4 +sourcery_version=2.2.5 if which sourcery >/dev/null; then if [[ $(sourcery --version) != $sourcery_version ]]; then diff --git a/Tests/TestUtils/Stubs.swift b/Tests/TestUtils/Stubs.swift index 62c70d3ef..7bf866394 100644 --- a/Tests/TestUtils/Stubs.swift +++ b/Tests/TestUtils/Stubs.swift @@ -132,6 +132,7 @@ extension CompactBlockProcessor.Configuration { alias: alias, fsBlockCacheRoot: pathProvider.fsCacheURL, dataDb: pathProvider.dataDbURL, + torDir: pathProvider.torDirURL, spendParamsURL: pathProvider.spendParamsURL, outputParamsURL: pathProvider.outputParamsURL, saplingParamsSourceURL: SaplingParamsSourceURL.tests, diff --git a/Tests/TestUtils/TestCoordinator.swift b/Tests/TestUtils/TestCoordinator.swift index e17103aaa..47a30ffdf 100644 --- a/Tests/TestUtils/TestCoordinator.swift +++ b/Tests/TestUtils/TestCoordinator.swift @@ -72,6 +72,7 @@ class TestCoordinator { fsBlockDbRoot: databases.fsCacheDbRoot, generalStorageURL: databases.generalStorageURL, dataDbURL: databases.dataDB, + torDirURL: databases.torDir, endpoint: endpoint, network: network, spendParamsURL: try __spendParamsURL(), @@ -216,6 +217,7 @@ extension TestCoordinator { alias: config.alias, fsBlockCacheRoot: config.fsBlockCacheRoot, dataDb: config.dataDb, + torDir: config.torDir, spendParamsURL: config.spendParamsURL, outputParamsURL: config.outputParamsURL, saplingParamsSourceURL: config.saplingParamsSourceURL, @@ -246,6 +248,7 @@ extension TestCoordinator { struct TemporaryTestDatabases { var fsCacheDbRoot: URL let generalStorageURL: URL + var torDir: URL var dataDB: URL } @@ -257,6 +260,7 @@ enum TemporaryDbBuilder { return TemporaryTestDatabases( fsCacheDbRoot: tempUrl.appendingPathComponent("fs_cache_\(timestamp)"), generalStorageURL: tempUrl.appendingPathComponent("general_storage_\(timestamp)"), + torDir: tempUrl.appendingPathComponent("tor_\(timestamp)"), dataDB: tempUrl.appendingPathComponent("data_db_\(timestamp).db") ) } diff --git a/Tests/TestUtils/Tests+Utils.swift b/Tests/TestUtils/Tests+Utils.swift index 08c35345a..65699a2a3 100644 --- a/Tests/TestUtils/Tests+Utils.swift +++ b/Tests/TestUtils/Tests+Utils.swift @@ -96,6 +96,10 @@ func __dataDbURL() throws -> URL { try __documentsDirectory().appendingPathComponent("data.db", isDirectory: false) } +func __torDirURL() throws -> URL { + try __documentsDirectory().appendingPathComponent("tor", isDirectory: true) +} + func __spendParamsURL() throws -> URL { try __documentsDirectory().appendingPathComponent("sapling-spend.params") } diff --git a/Tests/TestUtils/TestsData.swift b/Tests/TestUtils/TestsData.swift index 058c6a7b4..c3787a52d 100644 --- a/Tests/TestUtils/TestsData.swift +++ b/Tests/TestUtils/TestsData.swift @@ -17,6 +17,7 @@ class TestsData { fsBlockDbRoot: URL(fileURLWithPath: "/"), generalStorageURL: URL(fileURLWithPath: "/"), dataDbURL: URL(fileURLWithPath: "/"), + torDirURL: URL(fileURLWithPath: "/"), endpoint: LightWalletEndpointBuilder.default, network: ZcashNetworkBuilder.network(for: networkType), spendParamsURL: URL(fileURLWithPath: "/"),