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:
parent
bce8085690
commit
99c46d0979
10
CHANGELOG.md
10
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<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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue