From a7f73c39f3cb0345ebffd94f3aec4c7668deabf0 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 6 Jun 2024 02:53:40 +0100 Subject: [PATCH 1/6] Migrate to first pre-release of FFI 0.9.0 Includes: - Initialization changes to enable log filter customization. We now connect the Rust log level to the Swift log level, and always run other Rust initialization steps. - ZIP 320 support (TEX addresses). --- .../xcshareddata/swiftpm/Package.resolved | 3 +- .../ZcashLightClientSample/AppDelegate.swift | 3 +- .../SyncBlocksListViewController.swift | 3 +- Package.resolved | 3 +- Package.swift | 4 +- .../Error/Sourcery/generateErrorCode.sh | 4 +- .../Error/ZcashError.swift | 7 ++- .../Error/ZcashErrorCode.swift | 4 +- .../Error/ZcashErrorCodeDefinition.swift | 3 ++ Sources/ZcashLightClientKit/Initializer.swift | 18 +++----- .../Model/WalletTypes.swift | 40 ++++++++++++++++- .../Rust/ZcashRustBackend.swift | 44 +++++++++++++++---- .../Synchronizer/Dependencies.swift | 35 +++++++++++++-- .../Tool/DerivationTool.swift | 16 +++++++ .../Utils/LoggingProxy.swift | 4 ++ .../ZcashLightClientKit/Utils/OSLogger.swift | 4 ++ .../AutoMockable.generated.swift | 20 ++++++++- Tests/TestUtils/Sourcery/generateMocks.sh | 2 +- 18 files changed, 177 insertions(+), 40 deletions(-) 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..45238818a 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift @@ -52,8 +52,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { network: kZcashNetwork, spendParamsURL: try! spendParamsURLHelper(), outputParamsURL: try! outputParamsURLHelper(), - saplingParamsSourceURL: SaplingParamsSourceURL.default, - enableBackendTracing: true + saplingParamsSourceURL: SaplingParamsSourceURL.default ) self.wallet = wallet diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift index 1904de453..96aef2aef 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift @@ -108,8 +108,7 @@ class SyncBlocksListViewController: UIViewController { 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/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..cf6fac6fe 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 /* @@ -509,6 +509,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 @@ -757,6 +760,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." @@ -937,6 +941,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..b68dca942 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 /* @@ -289,6 +289,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..2f8a25d69 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift @@ -571,6 +571,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..124af8ff8 100644 --- a/Sources/ZcashLightClientKit/Initializer.swift +++ b/Sources/ZcashLightClientKit/Initializer.swift @@ -164,8 +164,7 @@ public class Initializer { outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, alias: ZcashSynchronizerAlias = .default, - loggingPolicy: LoggingPolicy = .default(.debug), - enableBackendTracing: Bool = false + loggingPolicy: LoggingPolicy = .default(.debug) ) { let container = DIContainer() @@ -183,8 +182,7 @@ public class Initializer { outputParamsURL: outputParamsURL, saplingParamsSourceURL: saplingParamsSourceURL, alias: alias, - loggingPolicy: loggingPolicy, - enableBackendTracing: enableBackendTracing + loggingPolicy: loggingPolicy ) self.init( @@ -213,8 +211,7 @@ public class Initializer { 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. @@ -230,8 +227,7 @@ public class Initializer { outputParamsURL: outputParamsURL, saplingParamsSourceURL: saplingParamsSourceURL, alias: alias, - loggingPolicy: loggingPolicy, - enableBackendTracing: enableBackendTracing + loggingPolicy: loggingPolicy ) self.init( @@ -292,8 +288,7 @@ public class Initializer { outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, alias: ZcashSynchronizerAlias, - loggingPolicy: LoggingPolicy = .default(.debug), - enableBackendTracing: Bool = false + loggingPolicy: LoggingPolicy = .default(.debug) ) -> (URLs, ZcashError?) { let urls = URLs( fsBlockDbRoot: fsBlockDbRoot, @@ -313,8 +308,7 @@ public class Initializer { alias: alias, networkType: network.networkType, endpoint: endpoint, - loggingPolicy: loggingPolicy, - enableBackendTracing: enableBackendTracing + loggingPolicy: loggingPolicy ) return (updatedURLs, parsingError) 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/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 6a0034f46..b602da163 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,8 +847,10 @@ 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) + } } } 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/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/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/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 7d2798e3f..5f3b41ceb 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 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 From bce8085690eb1a4b74a062928627c1194023b632 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 6 Jun 2024 02:53:40 +0100 Subject: [PATCH 2/6] Add APIs for fetching the ZEC-USD exchange rate over Tor. --- CHANGELOG.md | 4 ++ .../ZcashLightClientSample/AppDelegate.swift | 10 ++++ .../GetBalanceViewController.swift | 12 +++-- .../SyncBlocksListViewController.swift | 1 + .../Block/CompactBlockProcessor.swift | 6 +++ .../ClosureSynchronizer.swift | 5 +- .../CombineSynchronizer.swift | 3 ++ .../Constants/ZcashSDK.swift | 12 +++++ .../Error/ZcashError.swift | 14 ++++++ .../Error/ZcashErrorCode.swift | 4 ++ .../Error/ZcashErrorCodeDefinition.swift | 8 ++++ Sources/ZcashLightClientKit/Initializer.swift | 14 ++++++ .../Providers/ResourceProvider.swift | 10 ++++ .../Rust/ZcashRustBackend.swift | 30 ++++++------ .../ZcashLightClientKit/Synchronizer.swift | 3 ++ .../Synchronizer/ClosureSDKSynchronizer.swift | 6 +++ .../Synchronizer/CombineSDKSynchronizer.swift | 6 +++ .../Synchronizer/SDKSynchronizer.swift | 18 +++++++ .../ZcashLightClientKit/Tor/TorClient.swift | 48 +++++++++++++++++++ .../TransactionEnhancementTests.swift | 2 + Tests/NetworkTests/BlockStreamingTest.swift | 1 + .../CompactBlockProcessorTests.swift | 2 + .../NetworkTests/CompactBlockReorgTests.swift | 2 + Tests/NetworkTests/DownloadTests.swift | 1 + .../MigrateLegacyCacheDBActionTests.swift | 1 + .../InitializerOfflineTests.swift | 40 ++++++++++++++++ .../SynchronizerOfflineTests.swift | 2 + Tests/OfflineTests/WalletTests.swift | 1 + .../PerformanceTests/SynchronizerTests.swift | 1 + Tests/TestUtils/DarkSideWalletService.swift | 6 ++- .../AutoMockable.generated.swift | 22 +++++++++ Tests/TestUtils/Stubs.swift | 1 + Tests/TestUtils/TestCoordinator.swift | 4 ++ Tests/TestUtils/Tests+Utils.swift | 4 ++ Tests/TestUtils/TestsData.swift | 1 + 35 files changed, 283 insertions(+), 22 deletions(-) create mode 100644 Sources/ZcashLightClientKit/Tor/TorClient.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index afa5dad36..25ca6d3c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). # Unreleased +## Added +- `Synchronizer.getExchangeRateUSD() -> NSDecimalNumber`, which fetches the latest USD/ZEC + exchange rate. Prices are queried over Tor (to hide the wallet's IP address) on Binance, + Coinbase, and Gemini. # 2.1.12 - 2024-07-04 diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift index 45238818a..a07bd91bd 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift @@ -48,6 +48,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { fsBlockDbRoot: try! fsBlockDbRootURLHelper(), generalStorageURL: try! generalStorageURLHelper(), dataDbURL: try! dataDbURLHelper(), + torDirURL: try! torDirURLHelper(), endpoint: DemoAppConfig.endpoint, network: kZcashNetwork, spendParamsURL: try! spendParamsURLHelper(), @@ -198,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..37c7a9026 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift @@ -19,10 +19,14 @@ class GetBalanceViewController: UIViewController { 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" + let balance = try? await synchronizer.getAccountBalance() + let balanceText = (balance?.saplingBalance.total().formattedString) ?? "0.0" + let verifiedText = (balance?.saplingBalance.spendableValue.formattedString) ?? "0.0" + let usdZecRate = try await synchronizer.getExchangeRateUSD() + let usdBalance = (balance?.saplingBalance.total().decimalValue ?? 0).multiplying(by: usdZecRate) + let usdVerified = (balance?.saplingBalance.spendableValue.decimalValue ?? 0).multiplying(by: usdZecRate) + self.balance.text = "\(balanceText) ZEC\n\(usdBalance) USD\n\n(\(usdZecRate) USD/ZEC)" + self.verified.text = "\(verifiedText) ZEC\n\(usdVerified) USD" } } } diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift index 96aef2aef..acddf9ff4 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift @@ -102,6 +102,7 @@ class SyncBlocksListViewController: UIViewController { fsBlockDbRoot: try! fsBlockDbRootURLHelper(), generalStorageURL: try! generalStorageURLHelper(), dataDbURL: try! dataDbURLHelper(), + torDirURL: try! torDirURLHelper(), endpoint: DemoAppConfig.endpoint, network: kZcashNetwork, spendParamsURL: try! spendParamsURLHelper(), 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..02fcc6d4a 100644 --- a/Sources/ZcashLightClientKit/ClosureSynchronizer.swift +++ b/Sources/ZcashLightClientKit/ClosureSynchronizer.swift @@ -128,7 +128,10 @@ public protocol ClosureSynchronizer { func refreshUTXOs(address: TransparentAddress, from height: BlockHeight, completion: @escaping (Result) -> Void) func getAccountBalance(accountIndex: Int, completion: @escaping (Result) -> Void) - + + /// Fetches the latest ZEC-USD exchange rate. + func getExchangeRateUSD(completion: @escaping (Result) -> Void) + /* 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..bbd24c0cb 100644 --- a/Sources/ZcashLightClientKit/CombineSynchronizer.swift +++ b/Sources/ZcashLightClientKit/CombineSynchronizer.swift @@ -131,6 +131,9 @@ public protocol CombineSynchronizer { func getAccountBalance(accountIndex: Int) -> SinglePublisher + /// Fetches the latest ZEC-USD exchange rate. + func getExchangeRateUSD() -> SinglePublisher + 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/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index cf6fac6fe..d07054fba 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -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 @@ -708,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." @@ -889,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 diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift index b68dca942..f5b905276 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -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. diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift index 2f8a25d69..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 diff --git a/Sources/ZcashLightClientKit/Initializer.swift b/Sources/ZcashLightClientKit/Initializer.swift index 124af8ff8..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,6 +160,7 @@ public class Initializer { fsBlockDbRoot: URL, generalStorageURL: URL, dataDbURL: URL, + torDirURL: URL, endpoint: LightWalletEndpoint, network: ZcashNetwork, spendParamsURL: URL, @@ -176,6 +179,7 @@ public class Initializer { fsBlockDbRoot: fsBlockDbRoot, generalStorageURL: generalStorageURL, dataDbURL: dataDbURL, + torDirURL: torDirURL, endpoint: endpoint, network: network, spendParamsURL: spendParamsURL, @@ -205,6 +209,7 @@ public class Initializer { fsBlockDbRoot: URL, generalStorageURL: URL, dataDbURL: URL, + torDirURL: URL, endpoint: LightWalletEndpoint, network: ZcashNetwork, spendParamsURL: URL, @@ -221,6 +226,7 @@ public class Initializer { fsBlockDbRoot: fsBlockDbRoot, generalStorageURL: generalStorageURL, dataDbURL: dataDbURL, + torDirURL: torDirURL, endpoint: endpoint, network: network, spendParamsURL: spendParamsURL, @@ -260,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 @@ -282,6 +289,7 @@ public class Initializer { fsBlockDbRoot: URL, generalStorageURL: URL, dataDbURL: URL, + torDirURL: URL, endpoint: LightWalletEndpoint, network: ZcashNetwork, spendParamsURL: URL, @@ -293,6 +301,7 @@ public class Initializer { let urls = URLs( fsBlockDbRoot: fsBlockDbRoot, dataDbURL: dataDbURL, + torDirURL: torDirURL, generalStorageURL: generalStorageURL, spendParamsURL: spendParamsURL, outputParamsURL: outputParamsURL @@ -356,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)) } @@ -372,6 +385,7 @@ public class Initializer { URLs( fsBlockDbRoot: updatedFsBlockDbRoot, dataDbURL: updatedDataDbURL, + torDirURL: updatedTorDirURL, generalStorageURL: updatedGeneralStorageURL, spendParamsURL: updatedSpendParamsURL, outputParamsURL: updateOutputParamsURL 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 b602da163..c01e4492b 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -854,28 +854,26 @@ private extension ZcashRustBackend { } } -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..e584294f8 100644 --- a/Sources/ZcashLightClientKit/Synchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer.swift @@ -309,6 +309,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. + func getExchangeRateUSD() async throws -> NSDecimalNumber + /// 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..2ce9f2f56 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift @@ -194,6 +194,12 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer { } } + public func getExchangeRateUSD(completion: @escaping (Result) -> Void) { + AsyncToClosureGateway.executeThrowingAction(completion) { + try await self.synchronizer.getExchangeRateUSD() + } + } + /* 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..9f09333cb 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift @@ -196,6 +196,12 @@ extension CombineSDKSynchronizer: CombineSynchronizer { } } + public func getExchangeRateUSD() -> SinglePublisher { + AsyncToCombineGateway.executeThrowingAction() { + try await self.synchronizer.getExchangeRateUSD() + } + } + public func rewind(_ policy: RewindPolicy) -> CompletablePublisher { synchronizer.rewind(policy) } public func wipe() -> CompletablePublisher { synchronizer.wipe() } } diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index 671c2d605..27a8f52c1 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -508,6 +508,24 @@ public class SDKSynchronizer: Synchronizer { try await initializer.rustBackend.getWalletSummary()?.accountBalances[UInt32(accountIndex)] } + public func getExchangeRateUSD() async throws -> NSDecimalNumber { + logger.info("Bootstrapping Tor client for fetching exchange rates") + let tor: TorClient + do { + tor = try await TorClient(torDir: initializer.torDirURL) + } catch { + logger.error("failed to bootstrap Tor client: \(error)") + throw error + } + + do { + return try await tor.getExchangeRateUSD() + } catch { + logger.error("Failed to fetch exchange rate through Tor: \(error)") + throw error + } + } + public func getUnifiedAddress(accountIndex: Int) async throws -> UnifiedAddress { try await blockProcessor.getUnifiedAddress(accountIndex: accountIndex) } diff --git a/Sources/ZcashLightClientKit/Tor/TorClient.swift b/Sources/ZcashLightClientKit/Tor/TorClient.swift new file mode 100644 index 000000000..cb14b394e --- /dev/null +++ b/Sources/ZcashLightClientKit/Tor/TorClient.swift @@ -0,0 +1,48 @@ +// +// TorRuntime.swift +// +// +// Created by Jack Grigg on 04/06/2024. +// + +import Foundation +import libzcashlc + +public class TorClient { + private let runtime: OpaquePointer + + 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 -> NSDecimalNumber { + let rate = zcashlc_get_exchange_rate_usd(runtime) + + if rate.is_sign_negative { + throw ZcashError.rustTorClientGet(lastErrorMessage(fallback: "`TorClient.get` failed with unknown error")) + } + + return NSDecimalNumber(mantissa: rate.mantissa, exponent: rate.exponent, isNegative: rate.is_sign_negative) + } +} 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 5f3b41ceb..5cde510e8 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -1798,6 +1798,28 @@ class SynchronizerMock: Synchronizer { } } + // MARK: - getExchangeRateUSD + + var getExchangeRateUSDThrowableError: Error? + var getExchangeRateUSDCallsCount = 0 + var getExchangeRateUSDCalled: Bool { + return getExchangeRateUSDCallsCount > 0 + } + var getExchangeRateUSDReturnValue: NSDecimalNumber! + var getExchangeRateUSDClosure: (() async throws -> NSDecimalNumber)? + + func getExchangeRateUSD() async throws -> NSDecimalNumber { + if let error = getExchangeRateUSDThrowableError { + throw error + } + getExchangeRateUSDCallsCount += 1 + if let closure = getExchangeRateUSDClosure { + return try await closure() + } else { + return getExchangeRateUSDReturnValue + } + } + // MARK: - rewind var rewindCallsCount = 0 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: "/"), From 99c46d0979ce6115e570b77f388d251bc6e96ad9 Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Wed, 31 Jul 2024 14:22:30 +0200 Subject: [PATCH 3/6] refresh-rates - API refactored to Combine's CurrentValueSubject refresh-rate - FiatCurrencyResult is now Equatable refresh-rates - cleanup refresh-rates - The API has been refactored to follow the same principles as for state and events. - Review comments addressed refresh-rates - The API has been extended to send a result of the operation, success or failure refresh-rates - bugfix of the try vs try? refresh-rates - reverted the error state Update CHANGELOG.md - changelog updated --- CHANGELOG.md | 10 +++-- .../GetBalanceViewController.swift | 42 +++++++++++++++---- .../ClosureSynchronizer.swift | 3 +- .../CombineSynchronizer.swift | 5 +-- .../Model/FiatCurrencyResult.swift | 13 ++++++ .../ZcashLightClientKit/Synchronizer.swift | 7 +++- .../Synchronizer/ClosureSDKSynchronizer.swift | 6 +-- .../Synchronizer/CombineSDKSynchronizer.swift | 6 +-- .../Synchronizer/SDKSynchronizer.swift | 26 ++++++------ .../ZcashLightClientKit/Tor/TorClient.swift | 7 +++- .../AutoMockable.generated.swift | 29 ++++++------- 11 files changed, 93 insertions(+), 61 deletions(-) create mode 100644 Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ca6d3c2..7a9ea3109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). # Unreleased + ## Added -- `Synchronizer.getExchangeRateUSD() -> NSDecimalNumber`, which fetches the latest USD/ZEC - exchange rate. Prices are queried over Tor (to hide the wallet's IP address) on Binance, - Coinbase, and Gemini. +- `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 diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift index 37c7a9026..89fe2cf96 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Get Balance/GetBalanceViewController.swift @@ -8,25 +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 balance = try? await synchronizer.getAccountBalance() - let balanceText = (balance?.saplingBalance.total().formattedString) ?? "0.0" - let verifiedText = (balance?.saplingBalance.spendableValue.formattedString) ?? "0.0" - let usdZecRate = try await synchronizer.getExchangeRateUSD() - let usdBalance = (balance?.saplingBalance.total().decimalValue ?? 0).multiplying(by: usdZecRate) - let usdVerified = (balance?.saplingBalance.spendableValue.decimalValue ?? 0).multiplying(by: usdZecRate) - self.balance.text = "\(balanceText) ZEC\n\(usdBalance) USD\n\n(\(usdZecRate) USD/ZEC)" - self.verified.text = "\(verifiedText) ZEC\n\(usdVerified) USD" + 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/Sources/ZcashLightClientKit/ClosureSynchronizer.swift b/Sources/ZcashLightClientKit/ClosureSynchronizer.swift index 02fcc6d4a..00d2583a5 100644 --- a/Sources/ZcashLightClientKit/ClosureSynchronizer.swift +++ b/Sources/ZcashLightClientKit/ClosureSynchronizer.swift @@ -129,8 +129,7 @@ public protocol ClosureSynchronizer { func getAccountBalance(accountIndex: Int, completion: @escaping (Result) -> Void) - /// Fetches the latest ZEC-USD exchange rate. - func getExchangeRateUSD(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 diff --git a/Sources/ZcashLightClientKit/CombineSynchronizer.swift b/Sources/ZcashLightClientKit/CombineSynchronizer.swift index bbd24c0cb..295791985 100644 --- a/Sources/ZcashLightClientKit/CombineSynchronizer.swift +++ b/Sources/ZcashLightClientKit/CombineSynchronizer.swift @@ -129,10 +129,7 @@ public protocol CombineSynchronizer { func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) -> SinglePublisher - func getAccountBalance(accountIndex: Int) -> SinglePublisher - - /// Fetches the latest ZEC-USD exchange rate. - func getExchangeRateUSD() -> SinglePublisher + func refreshExchangeRateUSD() func rewind(_ policy: RewindPolicy) -> CompletablePublisher func wipe() -> CompletablePublisher diff --git a/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift b/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift new file mode 100644 index 000000000..06af28e76 --- /dev/null +++ b/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift @@ -0,0 +1,13 @@ +// +// FiatCurrencyResult.swift +// +// +// Created by Lukáš Korba on 31.07.2024. +// + +import Foundation + +public struct FiatCurrencyResult: Equatable { + public let rate: NSDecimalNumber + public let date: Date +} diff --git a/Sources/ZcashLightClientKit/Synchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer.swift index e584294f8..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,8 +312,8 @@ 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. - func getExchangeRateUSD() async throws -> NSDecimalNumber + /// Fetches the latest ZEC-USD exchange rate and updates `exchangeRateUSDSubject`. + func refreshExchangeRateUSD() /// Rescans the known blocks with the current keys. /// diff --git a/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift index 2ce9f2f56..5c54ac69d 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift @@ -194,10 +194,8 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer { } } - public func getExchangeRateUSD(completion: @escaping (Result) -> Void) { - AsyncToClosureGateway.executeThrowingAction(completion) { - try await self.synchronizer.getExchangeRateUSD() - } + public func refreshExchangeRateUSD() { + synchronizer.refreshExchangeRateUSD() } /* diff --git a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift index 9f09333cb..fd910bf84 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift @@ -196,10 +196,8 @@ extension CombineSDKSynchronizer: CombineSynchronizer { } } - public func getExchangeRateUSD() -> SinglePublisher { - AsyncToCombineGateway.executeThrowingAction() { - try await self.synchronizer.getExchangeRateUSD() - } + public func refreshExchangeRateUSD() { + synchronizer.refreshExchangeRateUSD() } public func rewind(_ policy: RewindPolicy) -> CompletablePublisher { synchronizer.rewind(policy) } diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index 27a8f52c1..c32407bd7 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -22,6 +22,9 @@ 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 @@ -508,21 +511,16 @@ public class SDKSynchronizer: Synchronizer { try await initializer.rustBackend.getWalletSummary()?.accountBalances[UInt32(accountIndex)] } - public func getExchangeRateUSD() async throws -> NSDecimalNumber { - logger.info("Bootstrapping Tor client for fetching exchange rates") - let tor: TorClient - do { - tor = try await TorClient(torDir: initializer.torDirURL) - } catch { - logger.error("failed to bootstrap Tor client: \(error)") - throw error - } + /// Fetches the latest ZEC-USD exchange rate. + public func refreshExchangeRateUSD() { + Task { + logger.info("Bootstrapping Tor client for fetching exchange rates") - do { - return try await tor.getExchangeRateUSD() - } catch { - logger.error("Failed to fetch exchange rate through Tor: \(error)") - throw error + guard let tor = try? await TorClient(torDir: initializer.torDirURL) else { + return + } + + exchangeRateUSDSubject.send(try? await tor.getExchangeRateUSD()) } } diff --git a/Sources/ZcashLightClientKit/Tor/TorClient.swift b/Sources/ZcashLightClientKit/Tor/TorClient.swift index cb14b394e..d5e51e368 100644 --- a/Sources/ZcashLightClientKit/Tor/TorClient.swift +++ b/Sources/ZcashLightClientKit/Tor/TorClient.swift @@ -36,13 +36,16 @@ public class TorClient { zcashlc_free_tor_runtime(runtime) } - public func getExchangeRateUSD() async throws -> NSDecimalNumber { + 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")) } - return NSDecimalNumber(mantissa: rate.mantissa, exponent: rate.exponent, isNegative: rate.is_sign_negative) + return FiatCurrencyResult( + rate: NSDecimalNumber(mantissa: rate.mantissa, exponent: rate.exponent, isNegative: rate.is_sign_negative), + date: Date() + ) } } diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 5cde510e8..9a7856e6a 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -1311,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 } } @@ -1798,26 +1802,17 @@ class SynchronizerMock: Synchronizer { } } - // MARK: - getExchangeRateUSD + // MARK: - refreshExchangeRateUSD - var getExchangeRateUSDThrowableError: Error? - var getExchangeRateUSDCallsCount = 0 - var getExchangeRateUSDCalled: Bool { - return getExchangeRateUSDCallsCount > 0 + var refreshExchangeRateUSDCallsCount = 0 + var refreshExchangeRateUSDCalled: Bool { + return refreshExchangeRateUSDCallsCount > 0 } - var getExchangeRateUSDReturnValue: NSDecimalNumber! - var getExchangeRateUSDClosure: (() async throws -> NSDecimalNumber)? + var refreshExchangeRateUSDClosure: (() -> Void)? - func getExchangeRateUSD() async throws -> NSDecimalNumber { - if let error = getExchangeRateUSDThrowableError { - throw error - } - getExchangeRateUSDCallsCount += 1 - if let closure = getExchangeRateUSDClosure { - return try await closure() - } else { - return getExchangeRateUSDReturnValue - } + func refreshExchangeRateUSD() { + refreshExchangeRateUSDCallsCount += 1 + refreshExchangeRateUSDClosure!() } // MARK: - rewind From 03ce9c2740a70b62c973c43c4accf45318fe5ceb Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Thu, 1 Aug 2024 13:34:16 +0200 Subject: [PATCH 4/6] Update CHANGELOG.md Co-authored-by: Jack Grigg --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a9ea3109..14aade1b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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.refreshExchangeRateUSD()`, which refreshes the rate returned by `Synchronizer.exchangeRateUSDStream`. Prices are queried over Tor (to hide the wallet's IP address). From 1ffeff0c4b3fd6c7a60e77c83a00850422c7cffa Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Mon, 5 Aug 2024 08:59:54 +0200 Subject: [PATCH 5/6] always-return-value - The API has been refactored to carry state and always broadcast a value, even in case of failed request always-return-value - ignore refresh request when one is already in flight always-return-value - ensure tor client is initialized always-return-value - don't pass nil value always-return-value - final touch, the tor initialization is sligthly more controlled and error potentially propagated. --- .../Model/FiatCurrencyResult.swift | 14 ++++++- .../Synchronizer/SDKSynchronizer.swift | 37 +++++++++++++++---- .../ZcashLightClientKit/Tor/TorClient.swift | 10 ++++- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift b/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift index 06af28e76..350dd344c 100644 --- a/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift +++ b/Sources/ZcashLightClientKit/Model/FiatCurrencyResult.swift @@ -7,7 +7,19 @@ import Foundation +/// The model representing currency for ZEC-XXX conversion. Initial implementation +/// provides only USD value. public struct FiatCurrencyResult: Equatable { - public let rate: NSDecimalNumber + 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/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index c32407bd7..bb7891d50 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -27,6 +27,7 @@ public class SDKSynchronizer: Synchronizer { 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 @@ -85,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 { @@ -513,14 +516,34 @@ public class SDKSynchronizer: Synchronizer { /// Fetches the latest ZEC-USD exchange rate. public func refreshExchangeRateUSD() { + // ignore refresh request when one is already in flight + if let latestState = tor?.cachedValue?.state, latestState == .fetching { + return + } + + // broadcast cached value but update the state + if let cachedValue = tor?.cachedValue { + var fetchingState = cachedValue + fetchingState.state = .fetching + + exchangeRateUSDSubject.send(fetchingState) + } + Task { - logger.info("Bootstrapping Tor client for fetching exchange rates") + 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?.cachedValue + errorState?.state = .error - guard let tor = try? await TorClient(torDir: initializer.torDirURL) else { - return + exchangeRateUSDSubject.send(errorState) } - - exchangeRateUSDSubject.send(try? await tor.getExchangeRateUSD()) } } diff --git a/Sources/ZcashLightClientKit/Tor/TorClient.swift b/Sources/ZcashLightClientKit/Tor/TorClient.swift index d5e51e368..e4be10dc9 100644 --- a/Sources/ZcashLightClientKit/Tor/TorClient.swift +++ b/Sources/ZcashLightClientKit/Tor/TorClient.swift @@ -10,6 +10,7 @@ import libzcashlc public class TorClient { private let runtime: OpaquePointer + public var cachedValue: FiatCurrencyResult? init(torDir: URL) async throws { // Ensure that the directory exists. @@ -43,9 +44,14 @@ public class TorClient { throw ZcashError.rustTorClientGet(lastErrorMessage(fallback: "`TorClient.get` failed with unknown error")) } - return FiatCurrencyResult( + let newValue = FiatCurrencyResult( + date: Date(), rate: NSDecimalNumber(mantissa: rate.mantissa, exponent: rate.exponent, isNegative: rate.is_sign_negative), - date: Date() + state: .success ) + + cachedValue = newValue + + return newValue } } From 03e7d6c92d30baee4af03051467e75de7c433b6b Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Tue, 13 Aug 2024 12:31:48 +0200 Subject: [PATCH 6/6] comments-fixed - comments addressed --- .../Synchronizer/SDKSynchronizer.swift | 10 ++++++---- Sources/ZcashLightClientKit/Tor/TorClient.swift | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index bb7891d50..1f03a2f93 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -517,14 +517,15 @@ public class SDKSynchronizer: Synchronizer { /// Fetches the latest ZEC-USD exchange rate. public func refreshExchangeRateUSD() { // ignore refresh request when one is already in flight - if let latestState = tor?.cachedValue?.state, latestState == .fetching { + if let latestState = tor?.cachedFiatCurrencyResult?.state, latestState == .fetching { return } // broadcast cached value but update the state - if let cachedValue = tor?.cachedValue { - var fetchingState = cachedValue + if let cachedFiatCurrencyResult = tor?.cachedFiatCurrencyResult { + var fetchingState = cachedFiatCurrencyResult fetchingState.state = .fetching + tor?.cachedFiatCurrencyResult = fetchingState exchangeRateUSDSubject.send(fetchingState) } @@ -539,8 +540,9 @@ public class SDKSynchronizer: Synchronizer { exchangeRateUSDSubject.send(try await tor?.getExchangeRateUSD()) } catch { // broadcast cached value but update the state - var errorState = tor?.cachedValue + var errorState = tor?.cachedFiatCurrencyResult errorState?.state = .error + tor?.cachedFiatCurrencyResult = errorState exchangeRateUSDSubject.send(errorState) } diff --git a/Sources/ZcashLightClientKit/Tor/TorClient.swift b/Sources/ZcashLightClientKit/Tor/TorClient.swift index e4be10dc9..2c3707606 100644 --- a/Sources/ZcashLightClientKit/Tor/TorClient.swift +++ b/Sources/ZcashLightClientKit/Tor/TorClient.swift @@ -10,7 +10,7 @@ import libzcashlc public class TorClient { private let runtime: OpaquePointer - public var cachedValue: FiatCurrencyResult? + public var cachedFiatCurrencyResult: FiatCurrencyResult? init(torDir: URL) async throws { // Ensure that the directory exists. @@ -50,7 +50,7 @@ public class TorClient { state: .success ) - cachedValue = newValue + cachedFiatCurrencyResult = newValue return newValue }