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
This commit is contained in:
Lukas Korba 2024-07-31 14:22:30 +02:00
parent bce8085690
commit 99c46d0979
11 changed files with 93 additions and 61 deletions

View File

@ -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<FiatCurrencyResult?, Never>`,
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

View File

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

View File

@ -129,8 +129,7 @@ public protocol ClosureSynchronizer {
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)
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

View File

@ -129,10 +129,7 @@ public protocol CombineSynchronizer {
func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) -> SinglePublisher<RefreshedUTXOs, Error>
func getAccountBalance(accountIndex: Int) -> SinglePublisher<AccountBalance?, Error>
/// Fetches the latest ZEC-USD exchange rate.
func getExchangeRateUSD() -> SinglePublisher<NSDecimalNumber, Error>
func refreshExchangeRateUSD()
func rewind(_ policy: RewindPolicy) -> CompletablePublisher<Error>
func wipe() -> CompletablePublisher<Error>

View File

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

View File

@ -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<SynchronizerEvent, Never> { get }
/// This stream emits the latest known USD/ZEC exchange rate, paired with the time it was queried. See `FiatCurrencyResult`.
var exchangeRateUSDStream: AnyPublisher<FiatCurrencyResult?, Never> { 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.
///

View File

@ -194,10 +194,8 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer {
}
}
public func getExchangeRateUSD(completion: @escaping (Result<NSDecimalNumber, Error>) -> Void) {
AsyncToClosureGateway.executeThrowingAction(completion) {
try await self.synchronizer.getExchangeRateUSD()
}
public func refreshExchangeRateUSD() {
synchronizer.refreshExchangeRateUSD()
}
/*

View File

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

View File

@ -22,6 +22,9 @@ public class SDKSynchronizer: Synchronizer {
private let eventSubject = PassthroughSubject<SynchronizerEvent, Never>()
public var eventStream: AnyPublisher<SynchronizerEvent, Never> { eventSubject.eraseToAnyPublisher() }
private let exchangeRateUSDSubject = CurrentValueSubject<FiatCurrencyResult?, Never>(nil)
public var exchangeRateUSDStream: AnyPublisher<FiatCurrencyResult?, Never> { 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())
}
}

View File

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

View File

@ -1311,6 +1311,10 @@ class SynchronizerMock: Synchronizer {
get { return underlyingEventStream }
}
var underlyingEventStream: AnyPublisher<SynchronizerEvent, Never>!
var exchangeRateUSDStream: AnyPublisher<FiatCurrencyResult?, Never> {
get { return underlyingExchangeRateUSDStream }
}
var underlyingExchangeRateUSDStream: AnyPublisher<FiatCurrencyResult?, Never>!
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