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.
- Fetching the ZEC-USD exchange rate over Tor.
This commit is contained in:
Jack Grigg 2024-06-06 02:53:40 +01:00
parent 4c92c2b4a6
commit 929f1983bd
42 changed files with 399 additions and 64 deletions

View File

@ -6,6 +6,11 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# 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.9 - 2024-06-05
## Fixed

View File

@ -176,8 +176,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "9314c83d7a09d88e1c0bd3ff3738a50833325059",
"version" : "0.8.0"
"revision" : "5919afaa2c37fa11946d43b5c1e66b8463867f98"
}
}
],

View File

@ -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")
}

View File

@ -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"
}
}
}

View File

@ -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)

View File

@ -122,8 +122,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "9314c83d7a09d88e1c0bd3ff3738a50833325059",
"version" : "0.8.0"
"revision" : "5919afaa2c37fa11946d43b5c1e66b8463867f98"
}
}
],

View File

@ -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.0")
//.package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", exact: "0.8.0")
// Compiled from 7c7b797211a7bd0b847df6315faa9ae13c32a0f6
.package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "5919afaa2c37fa11946d43b5c1e66b8463867f98")
],
targets: [
.target(

View File

@ -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,

View File

@ -128,7 +128,10 @@ public protocol ClosureSynchronizer {
func refreshUTXOs(address: TransparentAddress, from height: BlockHeight, completion: @escaping (Result<RefreshedUTXOs, Error>) -> Void)
func getAccountBalance(accountIndex: Int, completion: @escaping (Result<AccountBalance?, Error>) -> Void)
/// Fetches the latest ZEC-USD exchange rate.
func getExchangeRateUSD(completion: @escaping (Result<NSDecimalNumber, Error>) -> 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.

View File

@ -131,6 +131,9 @@ public protocol CombineSynchronizer {
func getAccountBalance(accountIndex: Int) -> SinglePublisher<AccountBalance?, Error>
/// Fetches the latest ZEC-USD exchange rate.
func getExchangeRateUSD() -> SinglePublisher<NSDecimalNumber, Error>
func rewind(_ policy: RewindPolicy) -> CompletablePublisher<Error>
func wipe() -> CompletablePublisher<Error>
}

View File

@ -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"

View File

@ -3,11 +3,11 @@
scriptDir=${0:a:h}
cd "${scriptDir}"
sourcery_version=2.1.7
sourcery_version=2.2.4
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 sourcer version not installed. Install sourcer $sourcery_version. Currently installed version is $(sourcery --version)"
exit 1
fi

View File

@ -1,4 +1,4 @@
// Generated using Sourcery 2.1.7 https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.2.4 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
@ -705,6 +715,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."
@ -885,6 +897,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

View File

@ -1,4 +1,4 @@
// Generated using Sourcery 2.1.7 https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.2.4 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.

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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)
}
}
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<Int8>.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
static func initializeRust(logLevel: RustLogging) {
logLevel.rawValue.utf8CString.withUnsafeBufferPointer { levelPtr in
zcashlc_init_on_load(levelPtr.baseAddress)
}
}
}
private extension URL {
nonisolated func lastErrorMessage(fallback: String) -> String {
let errorLen = zcashlc_last_error_length()
defer { zcashlc_clear_last_error() }
if errorLen > 0 {
let error = UnsafeMutablePointer<Int8>.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
}
}
extension URL {
func osStr() -> (String, UInt) {
let path = self.absoluteString
return (path, UInt(path.lengthOfBytes(using: .utf8)))

View File

@ -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

View File

@ -194,6 +194,12 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer {
}
}
public func getExchangeRateUSD(completion: @escaping (Result<NSDecimalNumber, Error>) -> 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.

View File

@ -196,6 +196,12 @@ extension CombineSDKSynchronizer: CombineSynchronizer {
}
}
public func getExchangeRateUSD() -> SinglePublisher<NSDecimalNumber, Error> {
AsyncToCombineGateway.executeThrowingAction() {
try await self.synchronizer.getExchangeRateUSD()
}
}
public func rewind(_ policy: RewindPolicy) -> CompletablePublisher<Error> { synchronizer.rewind(policy) }
public func wipe() -> CompletablePublisher<Error> { synchronizer.wipe() }
}

View File

@ -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
)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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) {}

View File

@ -39,6 +39,10 @@ public class OSLogger: Logger {
}
}
public func maxLogLevel() -> LogLevel? {
self.level
}
public func debug(
_ message: String,
file: StaticString = #file,

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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(),

View File

@ -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(),

View File

@ -206,7 +206,11 @@ enum DarksideWalletDConstants: NetworkConstants {
static var defaultDataDbName: String {
ZcashSDKMainnetConstants.defaultDataDbName
}
static var defaultTorDirName: String {
ZcashSDKMainnetConstants.defaultTorDirName
}
static var defaultCacheDbName: String {
ZcashSDKMainnetConstants.defaultCacheDbName
}

View File

@ -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
@ -1780,6 +1798,28 @@ class SynchronizerMock: Synchronizer {
}
}
// MARK: - getExchangeRateUSD
var getExchangeRateUSDThrowableError: Error?
var getExchangeRateUSDCallsCount = 0
var getExchangeRateUSDCalled: Bool {
return getExchangeRateUSDCallsCount > 0
}
var getExchangeRateUSDReturnValue: Float64!
var getExchangeRateUSDClosure: (() async throws -> Float64)?
func getExchangeRateUSD() async throws -> Float64 {
if let error = getExchangeRateUSDThrowableError {
throw error
}
getExchangeRateUSDCallsCount += 1
if let closure = getExchangeRateUSDClosure {
return try await closure()
} else {
return getExchangeRateUSDReturnValue
}
}
// MARK: - rewind
var rewindCallsCount = 0

View File

@ -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,

View File

@ -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")
)
}

View File

@ -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")
}

View File

@ -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: "/"),