Merge 16a80d2efb
into e5e826d133
This commit is contained in:
commit
afc33d8f64
|
@ -6,6 +6,14 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
# Unreleased
|
||||
|
||||
## Added
|
||||
- `SDKSynchronizer.estimateBirthdayHeight(for date: Date)`: Get an estimated height for a given date, typically used for estimating birthday.
|
||||
|
||||
## Changed
|
||||
- `LightWalletGRPCService` updated to use TOR connection for: fetching and submission of the transaction, getting server info, latest block height and tree state.
|
||||
|
||||
# 2.2.11 - 2025-04-03
|
||||
|
||||
## Fixed
|
||||
- `transparent_gap_limit_handling` migration, whereby wallets having received transparent outputs at child indices below the index of the default address could cause the migration to fail.
|
||||
|
||||
|
|
|
@ -176,8 +176,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Electric-Coin-Company/zcash-light-client-ffi",
|
||||
"state" : {
|
||||
"revision" : "78cc7388a2ba5530888a99e584823a7399631d48",
|
||||
"version" : "0.14.2"
|
||||
"branch" : "preview/release/0.15.0",
|
||||
"revision" : "c336dfc88b81aa3c857bc35e2d7d5452ae816d6e"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -21,14 +21,18 @@ enum DemoAppConfig {
|
|||
static let host = ZcashSDK.isMainnet ? "zec.rocks" : "lightwalletd.testnet.electriccoin.co"
|
||||
static let port: Int = 443
|
||||
|
||||
static let defaultBirthdayHeight: BlockHeight = ZcashSDK.isMainnet ? 935000 : 1386000
|
||||
static let defaultBirthdayHeight: BlockHeight = 2832500//ZcashSDK.isMainnet ? 935000 : 1386000
|
||||
|
||||
// static let defaultSeed = try! Mnemonic.deterministicSeedBytes(from: """
|
||||
// wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame
|
||||
// """)
|
||||
|
||||
// static let defaultSeed = try! Mnemonic.deterministicSeedBytes(from: """
|
||||
// live combine flight accident slow soda mind bright absent bid hen shy decade biology amazing mix enlist ensure biology rhythm snap duty soap armor
|
||||
// """)
|
||||
|
||||
static let defaultSeed = try! Mnemonic.deterministicSeedBytes(from: """
|
||||
live combine flight accident slow soda mind bright absent bid hen shy decade biology amazing mix enlist ensure biology rhythm snap duty soap armor
|
||||
wreck craft number between hard warfare wisdom leave radar host local crane float play logic whale clap parade dynamic cotton attitude people guard together
|
||||
""")
|
||||
|
||||
static let otherSynchronizers: [SynchronizerInitData] = [
|
||||
|
|
|
@ -319,8 +319,8 @@ extension SendViewController: UITextViewDelegate {
|
|||
extension SDKSynchronizer {
|
||||
static func textFor(state: SyncStatus) -> String {
|
||||
switch state {
|
||||
case .syncing(let progress):
|
||||
return "Syncing \(progress * 100.0)%"
|
||||
case let .syncing(syncProgress, areFundsSpendable):
|
||||
return "Syncing \(syncProgress * 100.0)% spendable: \(areFundsSpendable)"
|
||||
|
||||
case .upToDate:
|
||||
return "Up to Date 😎"
|
||||
|
|
|
@ -172,8 +172,8 @@ extension SyncBlocksListViewController: UITableViewDataSource {
|
|||
extension SyncStatus {
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .syncing(progress):
|
||||
return "Syncing 🤖 \(floor(progress * 1000) / 10)%"
|
||||
case let .syncing(syncProgress, areFundsSpendable):
|
||||
return "Syncing 🤖 \(floor(syncProgress * 1000) / 10)% spendable: \(areFundsSpendable)"
|
||||
case .upToDate:
|
||||
return "Up to Date 😎"
|
||||
case .unprepared:
|
||||
|
|
|
@ -68,11 +68,11 @@ class SyncBlocksViewController: UIViewController {
|
|||
case .unprepared:
|
||||
break
|
||||
|
||||
case let .syncing(progress):
|
||||
case let .syncing(syncProgress, areFundsSpendable):
|
||||
enhancingStarted = false
|
||||
|
||||
progressBar.progress = progress
|
||||
progressLabel.text = "\(floor(progress * 1000) / 10)%"
|
||||
progressBar.progress = syncProgress
|
||||
progressLabel.text = "\(floor(syncProgress * 1000) / 10)% spendable: \(areFundsSpendable)"
|
||||
let progressText = """
|
||||
latest block height \(state.latestBlockHeight)
|
||||
"""
|
||||
|
|
|
@ -16,7 +16,8 @@ let package = Package(
|
|||
dependencies: [
|
||||
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.24.2"),
|
||||
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.3"),
|
||||
.package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", exact: "0.14.2")
|
||||
// .package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", exact: "0.14.2")
|
||||
.package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", branch: "preview/release/0.15.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
|
|
@ -73,13 +73,35 @@ extension ScanAction: Action {
|
|||
// Proper solution is handled in
|
||||
// TODO: [#1353] Advanced progress reporting, https://github.com/Electric-Coin-Company/zcash-swift-wallet-sdk/issues/1353
|
||||
if progressReportReducer == 0 {
|
||||
let walletSummary = try? await rustBackend.getWalletSummary()
|
||||
let recoveryProgress = walletSummary?.recoveryProgress
|
||||
|
||||
// report scan progress only if it's available
|
||||
if let scanProgress = try? await rustBackend.getWalletSummary()?.scanProgress {
|
||||
logger.debug("progress ratio: \(scanProgress.numerator)/\(scanProgress.denominator)")
|
||||
let progress = try scanProgress.progress()
|
||||
if let scanProgress = walletSummary?.scanProgress {
|
||||
let composedNumerator: Float = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0)
|
||||
let composedDenominator: Float = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0)
|
||||
|
||||
logger.debug("progress ratio: \(composedNumerator)/\(composedDenominator)")
|
||||
|
||||
let progress: Float
|
||||
if composedDenominator == 0 {
|
||||
progress = 1.0
|
||||
} else {
|
||||
progress = composedNumerator / composedDenominator
|
||||
}
|
||||
|
||||
// this shouldn't happen but if it does, we need to get notified by clients and work on a fix
|
||||
if progress > 1.0 {
|
||||
throw ZcashError.rustScanProgressOutOfRange("\(progress)")
|
||||
}
|
||||
|
||||
let scanProgress: Float = (try? scanProgress.progress()) ?? 0.0
|
||||
let areFundsSpendable = scanProgress == 1.0
|
||||
|
||||
logger.debug("progress float: \(progress)")
|
||||
await didUpdate(.syncProgress(progress))
|
||||
await didUpdate(.syncProgress(progress, areFundsSpendable))
|
||||
}
|
||||
|
||||
progressReportReducer = Constants.reportDelay
|
||||
} else {
|
||||
progressReportReducer -= 1
|
||||
|
|
|
@ -483,10 +483,10 @@ extension CompactBlockProcessor {
|
|||
case handledReorg(_ reorgHeight: BlockHeight, _ rewindHeight: BlockHeight)
|
||||
|
||||
/// Event sent when progress of some specific action happened.
|
||||
case syncProgress(Float)
|
||||
case syncProgress(Float, Bool)
|
||||
|
||||
/// Event sent when progress of the sync process changes.
|
||||
case progressUpdated(Float)
|
||||
case progressUpdated(Float, Bool)
|
||||
|
||||
/// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them.
|
||||
case storedUTXOs((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]))
|
||||
|
@ -569,7 +569,9 @@ extension CompactBlockProcessor {
|
|||
await self?.send(event: event)
|
||||
if let progressChanged = await self?.compactBlockProgress.hasProgressUpdated(event), progressChanged {
|
||||
if let progress = await self?.compactBlockProgress.progress {
|
||||
await self?.send(event: .progressUpdated(progress))
|
||||
await self?.send(
|
||||
event: .progressUpdated(progress, self?.compactBlockProgress.areFundsSpendable ?? false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -713,7 +715,7 @@ extension CompactBlockProcessor {
|
|||
let lastScannedHeight = await latestBlocksDataProvider.maxScannedHeight
|
||||
// Some actions may not run. For example there are no transactions to enhance and therefore there is no enhance progress. And in
|
||||
// cases like this computation of final progress won't work properly. So let's fake 100% progress at the end of the sync process.
|
||||
await send(event: .progressUpdated(1))
|
||||
await send(event: .progressUpdated(1, false))
|
||||
await send(event: .finished(lastScannedHeight))
|
||||
await context.update(state: .finished)
|
||||
|
||||
|
@ -776,7 +778,7 @@ extension CompactBlockProcessor {
|
|||
guard let self else { return }
|
||||
if await self.isIdle() {
|
||||
if await self.canStartSync() {
|
||||
self.logger.debug(
|
||||
await self.logger.debug(
|
||||
"""
|
||||
Timer triggered: Starting compact Block processor!.
|
||||
Processor State: \(await self.context.state)
|
||||
|
|
|
@ -11,14 +11,16 @@ final actor CompactBlockProgress {
|
|||
static let zero = CompactBlockProgress()
|
||||
|
||||
var progress: Float = 0.0
|
||||
var areFundsSpendable: Bool = false
|
||||
|
||||
func hasProgressUpdated(_ event: CompactBlockProcessor.Event) -> Bool {
|
||||
guard case .syncProgress(let update) = event else {
|
||||
guard case let .syncProgress(progress, areFundsSpendable) = event else {
|
||||
return false
|
||||
}
|
||||
|
||||
progress = update
|
||||
|
||||
self.progress = progress
|
||||
self.areFundsSpendable = areFundsSpendable
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,4 +30,91 @@ struct BundleCheckpointSource: CheckpointSource {
|
|||
checkpointDirectory: BundleCheckpointURLProvider.default.url(self.network)
|
||||
) ?? saplingActivation
|
||||
}
|
||||
|
||||
func estimateBirthdayHeight(for date: Date) -> BlockHeight {
|
||||
// the average time between 2500 blocks during last 10 checkpoints (estimated March 31, 2025) is 52.33 hours for mainnet
|
||||
// the average time between 10,000 blocks during last 10 checkpoints (estimated March 31, 2025) is 134.93 hours for testnet
|
||||
let avgIntervalTime: TimeInterval = network == .mainnet ? 52.33 : 134.93
|
||||
let blockInterval: Double = network == .mainnet ? 2500 : 10_000
|
||||
let saplingActivationHeight = network == .mainnet
|
||||
? ZcashMainnet().constants.saplingActivationHeight
|
||||
: ZcashTestnet().constants.saplingActivationHeight
|
||||
|
||||
let latestCheckpoint = latestKnownCheckpoint()
|
||||
let latestCheckpointTime = TimeInterval(latestCheckpoint.time)
|
||||
|
||||
// above latest checkpoint, return it
|
||||
guard date.timeIntervalSince1970 < latestCheckpointTime else {
|
||||
return latestCheckpoint.height
|
||||
}
|
||||
|
||||
// Phase 1, estimate possible height
|
||||
let nowTimeIntervalSince1970 = Date().timeIntervalSince1970
|
||||
let timeDiff = (nowTimeIntervalSince1970 - date.timeIntervalSince1970) - (nowTimeIntervalSince1970 - latestCheckpointTime)
|
||||
let blockDiff = ((timeDiff / 3600) / avgIntervalTime) * blockInterval
|
||||
|
||||
var heightToLookAround = Double(Int(latestCheckpoint.height - Int(blockDiff)) / Int(blockInterval)) * blockInterval
|
||||
|
||||
// bellow sapling activation height
|
||||
guard Int(heightToLookAround) > saplingActivationHeight else {
|
||||
return saplingActivationHeight
|
||||
}
|
||||
|
||||
// Phase 2, load checkpoint and evaluate against given date
|
||||
guard let loadedCheckpoint = Checkpoint.birthday(
|
||||
with: BlockHeight(heightToLookAround),
|
||||
checkpointDirectory: BundleCheckpointURLProvider.default.url(self.network)
|
||||
) else {
|
||||
return saplingActivationHeight
|
||||
}
|
||||
|
||||
// loaded checkpoint is exactly the one
|
||||
var hoursApart = (TimeInterval(loadedCheckpoint.time) - date.timeIntervalSince1970) / 3600
|
||||
if hoursApart < 0 && abs(hoursApart) < avgIntervalTime {
|
||||
return loadedCheckpoint.height
|
||||
}
|
||||
|
||||
if hoursApart < 0 {
|
||||
// loaded checkpoint is lower, increase until reached the one
|
||||
var closestHeight = loadedCheckpoint.height
|
||||
while abs(hoursApart) > avgIntervalTime {
|
||||
heightToLookAround += blockInterval
|
||||
|
||||
if let loadedCheckpoint = Checkpoint.birthday(
|
||||
with: BlockHeight(heightToLookAround),
|
||||
checkpointDirectory: BundleCheckpointURLProvider.default.url(self.network)
|
||||
) {
|
||||
hoursApart = (TimeInterval(loadedCheckpoint.time) - date.timeIntervalSince1970) / 3600
|
||||
if hoursApart < 0 && abs(hoursApart) < avgIntervalTime {
|
||||
return loadedCheckpoint.height
|
||||
} else if hoursApart >= 0 {
|
||||
return closestHeight
|
||||
}
|
||||
|
||||
closestHeight = loadedCheckpoint.height
|
||||
} else {
|
||||
return saplingActivationHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// loaded checkpoint is higher, descrease until reached the one
|
||||
while hoursApart > 0 {
|
||||
heightToLookAround -= blockInterval
|
||||
|
||||
if let loadedCheckpoint = Checkpoint.birthday(
|
||||
with: BlockHeight(heightToLookAround),
|
||||
checkpointDirectory: BundleCheckpointURLProvider.default.url(self.network)
|
||||
) {
|
||||
hoursApart = (TimeInterval(loadedCheckpoint.time) - date.timeIntervalSince1970) / 3600
|
||||
if hoursApart < 0 {
|
||||
return loadedCheckpoint.height
|
||||
}
|
||||
} else {
|
||||
return saplingActivationHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saplingActivationHeight
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,8 @@ protocol CheckpointSource {
|
|||
/// - Note: When the user knows the exact height of the first received funds for a wallet,
|
||||
/// the effective birthday of that wallet is `transaction.height - 1`.
|
||||
func birthday(for height: BlockHeight) -> Checkpoint
|
||||
|
||||
/// Takes a given date and finds out the closes checkpoint's height for it.
|
||||
/// Each checkpoint has a timestamp stored so it can be used for the calculations.
|
||||
func estimateBirthdayHeight(for date: Date) -> BlockHeight
|
||||
}
|
||||
|
|
|
@ -156,6 +156,8 @@ public protocol ClosureSynchronizer {
|
|||
|
||||
func refreshExchangeRateUSD()
|
||||
|
||||
func estimateBirthdayHeight(for date: Date, completion: @escaping (BlockHeight) -> 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.
|
||||
|
|
|
@ -148,6 +148,8 @@ public protocol CombineSynchronizer {
|
|||
|
||||
func refreshExchangeRateUSD()
|
||||
|
||||
func estimateBirthdayHeight(for date: Date) -> SinglePublisher<BlockHeight, Error>
|
||||
|
||||
func rewind(_ policy: RewindPolicy) -> CompletablePublisher<Error>
|
||||
func wipe() -> CompletablePublisher<Error>
|
||||
}
|
||||
|
|
|
@ -420,6 +420,18 @@ public enum ZcashError: Equatable, Error {
|
|||
/// - `rustError` contains error generated by the rust layer.
|
||||
/// ZRUST0080
|
||||
case rustTorLwdSubmit(_ rustError: String)
|
||||
/// Error from rust layer when calling TorLwdConn.getInfo
|
||||
/// - `rustError` contains error generated by the rust layer.
|
||||
/// ZRUST0081
|
||||
case rustTorLwdGetInfo(_ rustError: String)
|
||||
/// Error from rust layer when calling TorLwdConn.latestBlockHeight
|
||||
/// - `rustError` contains error generated by the rust layer.
|
||||
/// ZRUST0082
|
||||
case rustTorLwdLatestBlockHeight(_ rustError: String)
|
||||
/// Error from rust layer when calling TorLwdConn.getTreeState
|
||||
/// - `rustError` contains error generated by the rust layer.
|
||||
/// ZRUST0083
|
||||
case rustTorLwdGetTreeState(_ rustError: String)
|
||||
/// SQLite query failed when fetching all accounts from the database.
|
||||
/// - `sqliteError` is error produced by SQLite library.
|
||||
/// ZADAO0001
|
||||
|
@ -802,6 +814,9 @@ public enum ZcashError: Equatable, Error {
|
|||
case .rustTorConnectToLightwalletd: return "Error from rust layer when calling TorClient.connectToLightwalletd"
|
||||
case .rustTorLwdFetchTransaction: return "Error from rust layer when calling TorLwdConn.fetchTransaction"
|
||||
case .rustTorLwdSubmit: return "Error from rust layer when calling TorLwdConn.submit"
|
||||
case .rustTorLwdGetInfo: return "Error from rust layer when calling TorLwdConn.getInfo"
|
||||
case .rustTorLwdLatestBlockHeight: return "Error from rust layer when calling TorLwdConn.latestBlockHeight"
|
||||
case .rustTorLwdGetTreeState: return "Error from rust layer when calling TorLwdConn.getTreeState"
|
||||
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."
|
||||
|
@ -1001,6 +1016,9 @@ public enum ZcashError: Equatable, Error {
|
|||
case .rustTorConnectToLightwalletd: return .rustTorConnectToLightwalletd
|
||||
case .rustTorLwdFetchTransaction: return .rustTorLwdFetchTransaction
|
||||
case .rustTorLwdSubmit: return .rustTorLwdSubmit
|
||||
case .rustTorLwdGetInfo: return .rustTorLwdGetInfo
|
||||
case .rustTorLwdLatestBlockHeight: return .rustTorLwdLatestBlockHeight
|
||||
case .rustTorLwdGetTreeState: return .rustTorLwdGetTreeState
|
||||
case .accountDAOGetAll: return .accountDAOGetAll
|
||||
case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode
|
||||
case .accountDAOFindBy: return .accountDAOFindBy
|
||||
|
|
|
@ -223,6 +223,12 @@ public enum ZcashErrorCode: String {
|
|||
case rustTorLwdFetchTransaction = "ZRUST0079"
|
||||
/// Error from rust layer when calling TorLwdConn.submit
|
||||
case rustTorLwdSubmit = "ZRUST0080"
|
||||
/// Error from rust layer when calling TorLwdConn.getInfo
|
||||
case rustTorLwdGetInfo = "ZRUST0081"
|
||||
/// Error from rust layer when calling TorLwdConn.latestBlockHeight
|
||||
case rustTorLwdLatestBlockHeight = "ZRUST0082"
|
||||
/// Error from rust layer when calling TorLwdConn.getTreeState
|
||||
case rustTorLwdGetTreeState = "ZRUST0083"
|
||||
/// SQLite query failed when fetching all accounts from the database.
|
||||
case accountDAOGetAll = "ZADAO0001"
|
||||
/// Fetched accounts from SQLite but can't decode them.
|
||||
|
|
|
@ -442,6 +442,18 @@ enum ZcashErrorDefinition {
|
|||
/// - `rustError` contains error generated by the rust layer.
|
||||
// sourcery: code="ZRUST0080"
|
||||
case rustTorLwdSubmit(_ rustError: String)
|
||||
/// Error from rust layer when calling TorLwdConn.getInfo
|
||||
/// - `rustError` contains error generated by the rust layer.
|
||||
// sourcery: code="ZRUST0081"
|
||||
case rustTorLwdGetInfo(_ rustError: String)
|
||||
/// Error from rust layer when calling TorLwdConn.latestBlockHeight
|
||||
/// - `rustError` contains error generated by the rust layer.
|
||||
// sourcery: code="ZRUST0082"
|
||||
case rustTorLwdLatestBlockHeight(_ rustError: String)
|
||||
/// Error from rust layer when calling TorLwdConn.getTreeState
|
||||
/// - `rustError` contains error generated by the rust layer.
|
||||
// sourcery: code="ZRUST0083"
|
||||
case rustTorLwdGetTreeState(_ rustError: String)
|
||||
|
||||
// MARK: - Account DAO
|
||||
|
||||
|
|
|
@ -42,8 +42,7 @@ public struct LightWalletEndpoint {
|
|||
}
|
||||
|
||||
var urlString: String {
|
||||
return String(
|
||||
format: "%@://%@:%d", secure ? "https" : "http", host, port)
|
||||
String(format: "%@://%@:%d", secure ? "https" : "http", host, port)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +53,7 @@ public struct SaplingParamsSourceURL {
|
|||
public let outputParamFileURL: URL
|
||||
|
||||
public static var `default`: SaplingParamsSourceURL {
|
||||
return SaplingParamsSourceURL(spendParamFileURL: ZcashSDK.spendParamFileURL, outputParamFileURL: ZcashSDK.outputParamFileURL)
|
||||
SaplingParamsSourceURL(spendParamFileURL: ZcashSDK.spendParamFileURL, outputParamFileURL: ZcashSDK.outputParamFileURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,7 @@ struct ScanProgress: Equatable {
|
|||
|
||||
func progress() throws -> Float {
|
||||
guard denominator != 0 else {
|
||||
// this shouldn't happen but if it does, we need to get notified by clients and work on a fix
|
||||
throw ZcashError.rustScanProgressOutOfRange("\(numerator)/\(denominator)")
|
||||
return 1.0
|
||||
}
|
||||
|
||||
let value = Float(numerator) / Float(denominator)
|
||||
|
@ -52,6 +51,7 @@ struct WalletSummary: Equatable {
|
|||
let accountBalances: [AccountUUID: AccountBalance]
|
||||
let chainTipHeight: BlockHeight
|
||||
let fullyScannedHeight: BlockHeight
|
||||
let recoveryProgress: ScanProgress?
|
||||
let scanProgress: ScanProgress?
|
||||
let nextSaplingSubtreeIndex: UInt32
|
||||
let nextOrchardSubtreeIndex: UInt32
|
||||
|
|
|
@ -61,6 +61,7 @@ class LightWalletGRPCService {
|
|||
let singleCallTimeout: TimeLimit
|
||||
let streamingCallTimeout: TimeLimit
|
||||
var latestBlockHeightProvider: LatestBlockHeightProvider = LiveLatestBlockHeightProvider()
|
||||
let torConn: TorLwdConn?
|
||||
|
||||
var connectionStateChange: ((_ from: ConnectionState, _ to: ConnectionState) -> Void)? {
|
||||
get { connectionManager.connectionStateChange }
|
||||
|
@ -69,13 +70,14 @@ class LightWalletGRPCService {
|
|||
|
||||
let queue: DispatchQueue
|
||||
|
||||
convenience init(endpoint: LightWalletEndpoint) {
|
||||
convenience init(endpoint: LightWalletEndpoint, torURL: URL?) {
|
||||
self.init(
|
||||
host: endpoint.host,
|
||||
port: endpoint.port,
|
||||
secure: endpoint.secure,
|
||||
singleCallTimeout: endpoint.singleCallTimeoutInMillis,
|
||||
streamingCallTimeout: endpoint.streamingCallTimeoutInMillis
|
||||
streamingCallTimeout: endpoint.streamingCallTimeoutInMillis,
|
||||
torURL: torURL
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -91,7 +93,8 @@ class LightWalletGRPCService {
|
|||
port: Int = 9067,
|
||||
secure: Bool = true,
|
||||
singleCallTimeout: Int64,
|
||||
streamingCallTimeout: Int64
|
||||
streamingCallTimeout: Int64,
|
||||
torURL: URL?
|
||||
) {
|
||||
self.connectionManager = ConnectionStatusManager()
|
||||
self.queue = DispatchQueue.init(label: "LightWalletGRPCService")
|
||||
|
@ -114,6 +117,13 @@ class LightWalletGRPCService {
|
|||
timeLimit: self.singleCallTimeout
|
||||
)
|
||||
)
|
||||
|
||||
var tor: TorClient? = nil
|
||||
if let torURL {
|
||||
tor = try? TorClient(torDir: torURL)
|
||||
}
|
||||
let endpointString = String(format: "%@://%@:%d", secure ? "https" : "http", host, port)
|
||||
self.torConn = try? tor?.connectToLightwalletd(endpoint: endpointString)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
@ -151,15 +161,23 @@ class LightWalletGRPCService {
|
|||
extension LightWalletGRPCService: LightWalletService {
|
||||
func getInfo() async throws -> LightWalletdInfo {
|
||||
do {
|
||||
return try await compactTxStreamer.getLightdInfo(Empty())
|
||||
if let torConn {
|
||||
return try torConn.getInfo()
|
||||
} else {
|
||||
return try await compactTxStreamer.getLightdInfo(Empty())
|
||||
}
|
||||
} catch {
|
||||
let serviceError = error.mapToServiceError()
|
||||
throw ZcashError.serviceGetInfoFailed(serviceError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func latestBlockHeight() async throws -> BlockHeight {
|
||||
try await latestBlockHeightProvider.latestBlockHeight(streamer: compactTxStreamer)
|
||||
if let torConn {
|
||||
return try torConn.latestBlockHeight()
|
||||
} else {
|
||||
return try await latestBlockHeightProvider.latestBlockHeight(streamer: compactTxStreamer)
|
||||
}
|
||||
}
|
||||
|
||||
func blockRange(_ range: CompactBlockRange) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
|
||||
|
@ -176,11 +194,15 @@ extension LightWalletGRPCService: LightWalletService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func submit(spendTransaction: Data) async throws -> LightWalletServiceResponse {
|
||||
do {
|
||||
let transaction = RawTransaction.with { $0.data = spendTransaction }
|
||||
return try await compactTxStreamer.sendTransaction(transaction)
|
||||
if let torConn {
|
||||
return try torConn.submit(spendTransaction: spendTransaction)
|
||||
} else {
|
||||
let transaction = RawTransaction.with { $0.data = spendTransaction }
|
||||
return try await compactTxStreamer.sendTransaction(transaction)
|
||||
}
|
||||
} catch {
|
||||
let serviceError = error.mapToServiceError()
|
||||
throw ZcashError.serviceSubmitFailed(serviceError)
|
||||
|
@ -188,37 +210,41 @@ extension LightWalletGRPCService: LightWalletService {
|
|||
}
|
||||
|
||||
func fetchTransaction(txId: Data) async throws -> (tx: ZcashTransaction.Fetched?, status: TransactionStatus) {
|
||||
var txFilter = TxFilter()
|
||||
txFilter.hash = txId
|
||||
|
||||
do {
|
||||
let rawTx = try await compactTxStreamer.getTransaction(txFilter)
|
||||
if let torConn {
|
||||
return try torConn.fetchTransaction(txId: txId)
|
||||
} else {
|
||||
var txFilter = TxFilter()
|
||||
txFilter.hash = txId
|
||||
|
||||
let isNotMined = rawTx.height == 0 || rawTx.height > UInt32.max
|
||||
|
||||
return (
|
||||
tx:
|
||||
ZcashTransaction.Fetched(
|
||||
rawID: txId,
|
||||
minedHeight: isNotMined ? nil : UInt32(rawTx.height),
|
||||
raw: rawTx.data
|
||||
),
|
||||
status: isNotMined ? .notInMainChain : .mined(Int(rawTx.height))
|
||||
)
|
||||
} catch let error as GRPCStatus {
|
||||
if error.makeGRPCStatus().code == .notFound {
|
||||
return (tx: nil, .txidNotRecognized)
|
||||
} else if let notFound = error.message?.contains("Transaction not found"), notFound {
|
||||
return (tx: nil, .txidNotRecognized)
|
||||
} else if let notFound = error.message?.contains("No such mempool or blockchain transaction. Use gettransaction for wallet transactions."), notFound {
|
||||
return (tx: nil, .txidNotRecognized)
|
||||
} else {
|
||||
do {
|
||||
let rawTx = try await compactTxStreamer.getTransaction(txFilter)
|
||||
|
||||
let isNotMined = rawTx.height == 0 || rawTx.height > UInt32.max
|
||||
|
||||
return (
|
||||
tx:
|
||||
ZcashTransaction.Fetched(
|
||||
rawID: txId,
|
||||
minedHeight: isNotMined ? nil : UInt32(rawTx.height),
|
||||
raw: rawTx.data
|
||||
),
|
||||
status: isNotMined ? .notInMainChain : .mined(Int(rawTx.height))
|
||||
)
|
||||
} catch let error as GRPCStatus {
|
||||
if error.makeGRPCStatus().code == .notFound {
|
||||
return (tx: nil, .txidNotRecognized)
|
||||
} else if let notFound = error.message?.contains("Transaction not found"), notFound {
|
||||
return (tx: nil, .txidNotRecognized)
|
||||
} else if let notFound = error.message?.contains("No such mempool or blockchain transaction. Use gettransaction for wallet transactions."), notFound {
|
||||
return (tx: nil, .txidNotRecognized)
|
||||
} else {
|
||||
let serviceError = error.mapToServiceError()
|
||||
throw ZcashError.serviceFetchTransactionFailed(serviceError)
|
||||
}
|
||||
} catch {
|
||||
let serviceError = error.mapToServiceError()
|
||||
throw ZcashError.serviceFetchTransactionFailed(serviceError)
|
||||
}
|
||||
} catch {
|
||||
let serviceError = error.mapToServiceError()
|
||||
throw ZcashError.serviceFetchTransactionFailed(serviceError)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,7 +325,11 @@ extension LightWalletGRPCService: LightWalletService {
|
|||
}
|
||||
|
||||
func getTreeState(_ id: BlockID) async throws -> TreeState {
|
||||
try await compactTxStreamer.getTreeState(id)
|
||||
if let torConn {
|
||||
return try torConn.getTreeState(height: BlockHeight(id.height))
|
||||
} else {
|
||||
return try await compactTxStreamer.getTreeState(id)
|
||||
}
|
||||
}
|
||||
|
||||
func getTaddressTxids(_ request: TransparentAddressBlockFilter) -> AsyncThrowingStream<RawTransaction, Error> {
|
||||
|
|
|
@ -136,9 +136,10 @@ protocol LightWalletServiceResponse {
|
|||
|
||||
struct LightWalletServiceFactory {
|
||||
let endpoint: LightWalletEndpoint
|
||||
let torURL: URL?
|
||||
|
||||
func make() -> LightWalletService {
|
||||
return LightWalletGRPCService(endpoint: endpoint)
|
||||
return LightWalletGRPCService(endpoint: endpoint, torURL: torURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -101,7 +101,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
|
|||
)
|
||||
|
||||
guard let ffiAddressPtr else {
|
||||
throw ZcashError.rustDeriveAddressFromUfvk(ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveAddressFromUfvk` failed with unknown error"))
|
||||
throw ZcashError.rustDeriveAddressFromUfvk(ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveAddressFromUfvk` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
defer { zcashlc_free_ffi_address(ffiAddressPtr) }
|
||||
|
@ -126,7 +127,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
|
|||
|
||||
guard let boxedSlice = boxedSlicePtr?.pointee else {
|
||||
throw ZcashError.rustDeriveUnifiedSpendingKey(
|
||||
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveUnifiedSpendingKey` failed with unknown error"))
|
||||
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveUnifiedSpendingKey` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
return boxedSlice.unsafeToUnifiedSpendingKey(network: networkType)
|
||||
|
@ -207,7 +209,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
|
|||
|
||||
guard let key = boxedSlicePtr?.pointee else {
|
||||
throw ZcashError.rustDeriveArbitraryWalletKey(
|
||||
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryWalletKey` failed with unknown error"))
|
||||
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryWalletKey` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
return key.ptr.toByteArray(
|
||||
|
@ -237,7 +240,8 @@ struct ZcashKeyDerivationBackend: ZcashKeyDerivationBackendWelding {
|
|||
|
||||
guard let key = boxedSlicePtr?.pointee else {
|
||||
throw ZcashError.rustDeriveArbitraryAccountKey(
|
||||
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryAccountKey` failed with unknown error"))
|
||||
ZcashKeyDerivationBackend.lastErrorMessage(fallback: "`deriveArbitraryAccountKey` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
return key.ptr.toByteArray(
|
||||
|
|
|
@ -909,6 +909,7 @@ struct ZcashRustBackend: ZcashRustBackendWelding {
|
|||
accountBalances: accountBalances,
|
||||
chainTipHeight: BlockHeight(summaryPtr.pointee.chain_tip_height),
|
||||
fullyScannedHeight: BlockHeight(summaryPtr.pointee.fully_scanned_height),
|
||||
recoveryProgress: summaryPtr.pointee.recovery_progress?.pointee.toScanProgress(),
|
||||
scanProgress: summaryPtr.pointee.scan_progress?.pointee.toScanProgress(),
|
||||
nextSaplingSubtreeIndex: UInt32(summaryPtr.pointee.next_sapling_subtree_index),
|
||||
nextOrchardSubtreeIndex: UInt32(summaryPtr.pointee.next_orchard_subtree_index)
|
||||
|
|
|
@ -426,13 +426,18 @@ public protocol Synchronizer: AnyObject {
|
|||
kServers: Int,
|
||||
network: NetworkType
|
||||
) async -> [LightWalletEndpoint]
|
||||
|
||||
/// Takes a given date and finds out the closes checkpoint's height for it.
|
||||
/// Each checkpoint has a timestamp stored so it can be used for the calculations.
|
||||
func estimateBirthdayHeight(for date: Date) -> BlockHeight
|
||||
}
|
||||
|
||||
public enum SyncStatus: Equatable {
|
||||
public static func == (lhs: SyncStatus, rhs: SyncStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.unprepared, .unprepared): return true
|
||||
case let (.syncing(lhsProgress), .syncing(rhsProgress)): return lhsProgress == rhsProgress
|
||||
case let (.syncing(lhsSyncProgress, lhsRecoveryPrgoress), .syncing(rhsSyncProgress, rhsRecoveryPrgoress)):
|
||||
return lhsSyncProgress == rhsSyncProgress && lhsRecoveryPrgoress == rhsRecoveryPrgoress
|
||||
case (.upToDate, .upToDate): return true
|
||||
case (.error, .error): return true
|
||||
default: return false
|
||||
|
@ -444,7 +449,7 @@ public enum SyncStatus: Equatable {
|
|||
/// taking other maintenance steps that need to occur after an upgrade.
|
||||
case unprepared
|
||||
|
||||
case syncing(_ progress: Float)
|
||||
case syncing(_ syncProgress: Float, _ areFundsSpendable: Bool)
|
||||
|
||||
/// Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
|
||||
/// When set, a UI element may want to turn green.
|
||||
|
@ -497,7 +502,7 @@ enum InternalSyncStatus: Equatable {
|
|||
case unprepared
|
||||
|
||||
/// Indicates that this Synchronizer is actively processing new blocks (consists of fetch, scan and enhance operations)
|
||||
case syncing(Float)
|
||||
case syncing(Float, Bool)
|
||||
|
||||
/// Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
|
||||
/// When set, a UI element may want to turn green.
|
||||
|
@ -593,7 +598,8 @@ extension InternalSyncStatus {
|
|||
public static func == (lhs: InternalSyncStatus, rhs: InternalSyncStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.unprepared, .unprepared): return true
|
||||
case let (.syncing(lhsProgress), .syncing(rhsProgress)): return lhsProgress == rhsProgress
|
||||
case let (.syncing(lhsSyncProgress, lhsRecoveryPrgoress), .syncing(rhsSyncProgress, rhsRecoveryPrgoress)):
|
||||
return lhsSyncProgress == rhsSyncProgress && lhsRecoveryPrgoress == rhsRecoveryPrgoress
|
||||
case (.synced, .synced): return true
|
||||
case (.stopped, .stopped): return true
|
||||
case (.disconnected, .disconnected): return true
|
||||
|
@ -604,8 +610,8 @@ extension InternalSyncStatus {
|
|||
}
|
||||
|
||||
extension InternalSyncStatus {
|
||||
init(_ blockProcessorProgress: Float) {
|
||||
self = .syncing(blockProcessorProgress)
|
||||
init(_ syncProgress: Float, _ areFundsSpendable: Bool) {
|
||||
self = .syncing(syncProgress, areFundsSpendable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -614,8 +620,8 @@ extension InternalSyncStatus {
|
|||
switch self {
|
||||
case .unprepared:
|
||||
return .unprepared
|
||||
case .syncing(let progress):
|
||||
return .syncing(progress)
|
||||
case let .syncing(syncProgress, areFundsSpendable):
|
||||
return .syncing(syncProgress, areFundsSpendable)
|
||||
case .synced:
|
||||
return .upToDate
|
||||
case .stopped:
|
||||
|
|
|
@ -256,6 +256,11 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer {
|
|||
synchronizer.refreshExchangeRateUSD()
|
||||
}
|
||||
|
||||
public func estimateBirthdayHeight(for date: Date, completion: @escaping (BlockHeight) -> Void) {
|
||||
let height = synchronizer.estimateBirthdayHeight(for: date)
|
||||
completion(height)
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
|
|
|
@ -254,6 +254,14 @@ extension CombineSDKSynchronizer: CombineSynchronizer {
|
|||
synchronizer.refreshExchangeRateUSD()
|
||||
}
|
||||
|
||||
public func estimateBirthdayHeight(for date: Date) -> SinglePublisher<BlockHeight, Error> {
|
||||
let height = synchronizer.estimateBirthdayHeight(for: date)
|
||||
let subject = PassthroughSubject<BlockHeight, Error>()
|
||||
subject.send(height)
|
||||
subject.send(completion: .finished)
|
||||
return subject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public func rewind(_ policy: RewindPolicy) -> CompletablePublisher<Error> { synchronizer.rewind(policy) }
|
||||
public func wipe() -> CompletablePublisher<Error> { synchronizer.wipe() }
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ enum Dependencies {
|
|||
}
|
||||
|
||||
container.register(type: LightWalletService.self, isSingleton: true) { _ in
|
||||
LightWalletGRPCService(endpoint: endpoint)
|
||||
LightWalletGRPCService(endpoint: endpoint, torURL: urls.torDirURL)
|
||||
}
|
||||
|
||||
container.register(type: TransactionRepository.self, isSingleton: true) { _ in
|
||||
|
|
|
@ -173,8 +173,34 @@ public class SDKSynchronizer: Synchronizer {
|
|||
await blockProcessor.start(retry: retry)
|
||||
|
||||
case .stopped, .synced, .disconnected, .error:
|
||||
let syncProgress = (try? await initializer.rustBackend.getWalletSummary()?.scanProgress?.progress()) ?? 0
|
||||
await updateStatus(.syncing(syncProgress))
|
||||
let walletSummary = try? await initializer.rustBackend.getWalletSummary()
|
||||
let recoveryProgress = walletSummary?.recoveryProgress
|
||||
|
||||
var syncProgress: Float = 0.0
|
||||
var areFundsSpendable = false
|
||||
|
||||
if let scanProgress = walletSummary?.scanProgress {
|
||||
let composedNumerator = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0)
|
||||
let composedDenominator = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0)
|
||||
|
||||
let progress: Float
|
||||
if composedDenominator == 0 {
|
||||
progress = 1.0
|
||||
} else {
|
||||
progress = composedNumerator / composedDenominator
|
||||
}
|
||||
|
||||
// this shouldn't happen but if it does, we need to get notified by clients and work on a fix
|
||||
if progress > 1.0 {
|
||||
throw ZcashError.rustScanProgressOutOfRange("\(progress)")
|
||||
}
|
||||
|
||||
let scanProgress: Float = (try? scanProgress.progress()) ?? 0.0
|
||||
areFundsSpendable = scanProgress == 1.0
|
||||
|
||||
syncProgress = progress
|
||||
}
|
||||
await updateStatus(.syncing(syncProgress, areFundsSpendable))
|
||||
await blockProcessor.start(retry: retry)
|
||||
}
|
||||
}
|
||||
|
@ -240,8 +266,8 @@ public class SDKSynchronizer: Synchronizer {
|
|||
// log reorg information
|
||||
self?.logger.info("handling reorg at: \(reorgHeight) with rewind height: \(rewindHeight)")
|
||||
|
||||
case let .progressUpdated(progress):
|
||||
await self?.progressUpdated(progress: progress)
|
||||
case let .progressUpdated(syncProgress, areFundsSpendable):
|
||||
await self?.progressUpdated(syncProgress, areFundsSpendable)
|
||||
|
||||
case .syncProgress:
|
||||
break
|
||||
|
@ -281,8 +307,8 @@ public class SDKSynchronizer: Synchronizer {
|
|||
}
|
||||
}
|
||||
|
||||
private func progressUpdated(progress: Float) async {
|
||||
let newStatus = InternalSyncStatus(progress)
|
||||
private func progressUpdated(_ syncProgress: Float, _ areFundsSpendable: Bool) async {
|
||||
let newStatus = InternalSyncStatus(syncProgress, areFundsSpendable)
|
||||
await updateStatus(newStatus)
|
||||
}
|
||||
|
||||
|
@ -723,7 +749,8 @@ public class SDKSynchronizer: Synchronizer {
|
|||
port: $0.port,
|
||||
secure: $0.secure,
|
||||
singleCallTimeout: 5000,
|
||||
streamingCallTimeout: Int64(fetchThresholdSeconds) * 1000
|
||||
streamingCallTimeout: Int64(fetchThresholdSeconds) * 1000,
|
||||
torURL: initializer.torDirURL
|
||||
),
|
||||
url: "\($0.host):\($0.port)"
|
||||
)
|
||||
|
@ -866,6 +893,10 @@ public class SDKSynchronizer: Synchronizer {
|
|||
return finalResult
|
||||
}
|
||||
|
||||
public func estimateBirthdayHeight(for date: Date) -> BlockHeight {
|
||||
initializer.container.resolve(CheckpointSource.self).estimateBirthdayHeight(for: date)
|
||||
}
|
||||
|
||||
// MARK: Server switch
|
||||
|
||||
public func switchTo(endpoint: LightWalletEndpoint) async throws {
|
||||
|
@ -877,13 +908,14 @@ public class SDKSynchronizer: Synchronizer {
|
|||
|
||||
// Validation of the server is first because any custom endpoint can be passed here
|
||||
// Extra instance of the service is created with lower timeout ofr a single call
|
||||
initializer.container.register(type: LightWalletService.self, isSingleton: true) { _ in
|
||||
initializer.container.register(type: LightWalletService.self, isSingleton: true) { [torURL = initializer.torDirURL] _ in
|
||||
LightWalletGRPCService(
|
||||
host: endpoint.host,
|
||||
port: endpoint.port,
|
||||
secure: endpoint.secure,
|
||||
singleCallTimeout: 5000,
|
||||
streamingCallTimeout: endpoint.streamingCallTimeoutInMillis
|
||||
streamingCallTimeout: endpoint.streamingCallTimeoutInMillis,
|
||||
torURL: torURL
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -904,8 +936,8 @@ public class SDKSynchronizer: Synchronizer {
|
|||
// SWITCH TO NEW ENDPOINT
|
||||
|
||||
// LightWalletService dependency update
|
||||
initializer.container.register(type: LightWalletService.self, isSingleton: true) { _ in
|
||||
LightWalletGRPCService(endpoint: endpoint)
|
||||
initializer.container.register(type: LightWalletService.self, isSingleton: true) { [torURL = initializer.torDirURL] _ in
|
||||
LightWalletGRPCService(endpoint: endpoint, torURL: torURL)
|
||||
}
|
||||
|
||||
// DEPENDENCIES
|
||||
|
|
|
@ -12,7 +12,7 @@ public class TorClient {
|
|||
private let runtime: OpaquePointer
|
||||
public var cachedFiatCurrencyResult: FiatCurrencyResult?
|
||||
|
||||
init(torDir: URL) async throws {
|
||||
init(torDir: URL) throws {
|
||||
// Ensure that the directory exists.
|
||||
let fileManager = FileManager()
|
||||
if !fileManager.fileExists(atPath: torDir.path) {
|
||||
|
@ -41,20 +41,19 @@ public class TorClient {
|
|||
zcashlc_free_tor_runtime(runtime)
|
||||
}
|
||||
|
||||
public func isolatedClient() async throws -> TorClient {
|
||||
public func isolatedClient() throws -> TorClient {
|
||||
let isolatedPtr = zcashlc_tor_isolated_client(runtime)
|
||||
|
||||
guard let isolatedPtr else {
|
||||
throw ZcashError.rustTorIsolatedClient(
|
||||
lastErrorMessage(
|
||||
fallback:
|
||||
"`TorClient.isolatedClient` failed with unknown error"))
|
||||
lastErrorMessage(fallback: "`TorClient.isolatedClient` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
return TorClient(runtimePtr: isolatedPtr)
|
||||
}
|
||||
|
||||
public func getExchangeRateUSD() async throws -> FiatCurrencyResult {
|
||||
public func getExchangeRateUSD() throws -> FiatCurrencyResult {
|
||||
let rate = zcashlc_get_exchange_rate_usd(runtime)
|
||||
|
||||
if rate.is_sign_negative {
|
||||
|
@ -64,8 +63,10 @@ public class TorClient {
|
|||
let newValue = FiatCurrencyResult(
|
||||
date: Date(),
|
||||
rate: NSDecimalNumber(
|
||||
mantissa: rate.mantissa, exponent: rate.exponent,
|
||||
isNegative: rate.is_sign_negative),
|
||||
mantissa: rate.mantissa,
|
||||
exponent: rate.exponent,
|
||||
isNegative: rate.is_sign_negative
|
||||
),
|
||||
state: .success
|
||||
)
|
||||
|
||||
|
@ -74,23 +75,19 @@ public class TorClient {
|
|||
return newValue
|
||||
}
|
||||
|
||||
public func connectToLightwalletd(endpoint: String) async throws
|
||||
-> TorLwdConn
|
||||
{
|
||||
public func connectToLightwalletd(endpoint: String) throws -> TorLwdConn {
|
||||
guard !endpoint.containsCStringNullBytesBeforeStringEnding() else {
|
||||
throw ZcashError.rustTorConnectToLightwalletd(
|
||||
"endpoint string contains null bytes")
|
||||
throw ZcashError.rustTorConnectToLightwalletd("endpoint string contains null bytes")
|
||||
}
|
||||
|
||||
let lwdConnPtr = zcashlc_tor_connect_to_lightwalletd(
|
||||
runtime, [CChar](endpoint.utf8CString))
|
||||
runtime, [CChar](endpoint.utf8CString)
|
||||
)
|
||||
|
||||
guard let lwdConnPtr else {
|
||||
throw ZcashError.rustTorConnectToLightwalletd(
|
||||
lastErrorMessage(
|
||||
fallback:
|
||||
"`TorClient.connectToLightwalletd` failed with unknown error"
|
||||
))
|
||||
lastErrorMessage(fallback: "`TorClient.connectToLightwalletd` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
return TorLwdConn(connPtr: lwdConnPtr)
|
||||
|
@ -100,7 +97,7 @@ public class TorClient {
|
|||
public class TorLwdConn {
|
||||
private let conn: OpaquePointer
|
||||
|
||||
fileprivate init(connPtr: OpaquePointer) {
|
||||
init(connPtr: OpaquePointer) {
|
||||
conn = connPtr
|
||||
}
|
||||
|
||||
|
@ -111,9 +108,7 @@ public class TorLwdConn {
|
|||
/// Submits a raw transaction over lightwalletd.
|
||||
/// - Parameter spendTransaction: data representing the transaction to be sent
|
||||
/// - Throws: `serviceSubmitFailed` when GRPC call fails.
|
||||
func submit(spendTransaction: Data) async throws
|
||||
-> LightWalletServiceResponse
|
||||
{
|
||||
func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
|
||||
let success = zcashlc_tor_lwd_conn_submit_transaction(
|
||||
conn,
|
||||
spendTransaction.bytes,
|
||||
|
@ -121,18 +116,21 @@ public class TorLwdConn {
|
|||
)
|
||||
|
||||
var response = SendResponse()
|
||||
|
||||
if !success {
|
||||
let err = lastErrorMessage(
|
||||
fallback: "`TorLwdConn.submit` failed with unknown error")
|
||||
if err.hasPrefix("Failed to submit transaction (")
|
||||
&& err.contains(")")
|
||||
{
|
||||
let startOfCode = err.firstIndex(of: "(")!
|
||||
let endOfCode = err.firstIndex(of: ")")!
|
||||
let errorCode = Int32(
|
||||
err[err.index(startOfCode, offsetBy: 1)..<endOfCode])!
|
||||
let errorMessage = String(
|
||||
err[err.index(endOfCode, offsetBy: 3)...])
|
||||
let err = lastErrorMessage(fallback: "`TorLwdConn.submit` failed with unknown error")
|
||||
|
||||
if err.hasPrefix("Failed to submit transaction (") && err.contains(")") {
|
||||
guard let startOfCode = err.firstIndex(of: "(") else {
|
||||
throw ZcashError.rustTorLwdSubmit(err)
|
||||
}
|
||||
guard let endOfCode = err.firstIndex(of: ")") else {
|
||||
throw ZcashError.rustTorLwdSubmit(err)
|
||||
}
|
||||
guard let errorCode = Int32(err[err.index(startOfCode, offsetBy: 1)..<endOfCode]) else {
|
||||
throw ZcashError.rustTorLwdSubmit(err)
|
||||
}
|
||||
let errorMessage = String(err[err.index(endOfCode, offsetBy: 3)...])
|
||||
|
||||
response.errorCode = errorCode
|
||||
response.errorMessage = errorMessage
|
||||
|
@ -140,6 +138,7 @@ public class TorLwdConn {
|
|||
throw ZcashError.rustTorLwdSubmit(err)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
@ -148,9 +147,7 @@ public class TorLwdConn {
|
|||
/// - Throws: LightWalletServiceError
|
||||
/// - Returns: LightWalletServiceResponse
|
||||
/// - Throws: `serviceFetchTransactionFailed` when GRPC call fails.
|
||||
func fetchTransaction(txId: Data) async throws -> (
|
||||
tx: ZcashTransaction.Fetched?, status: TransactionStatus
|
||||
) {
|
||||
func fetchTransaction(txId: Data) throws -> (tx: ZcashTransaction.Fetched?, status: TransactionStatus) {
|
||||
guard txId.count == 32 else {
|
||||
throw ZcashError.rustGetMemoInvalidTxIdLength
|
||||
}
|
||||
|
@ -161,10 +158,7 @@ public class TorLwdConn {
|
|||
|
||||
guard let txPtr else {
|
||||
throw ZcashError.rustTorLwdFetchTransaction(
|
||||
lastErrorMessage(
|
||||
fallback:
|
||||
"`TorLwdConn.fetchTransaction` failed with unknown error"
|
||||
)
|
||||
lastErrorMessage(fallback: "`TorLwdConn.fetchTransaction` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -185,4 +179,81 @@ public class TorLwdConn {
|
|||
status: isNotMined ? .notInMainChain : .mined(Int(height))
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets a lightwalletd server info
|
||||
/// - Returns: LightWalletdInfo
|
||||
func getInfo() throws -> LightWalletdInfo {
|
||||
let infoPtr = zcashlc_tor_lwd_conn_get_info(conn)
|
||||
|
||||
guard let infoPtr else {
|
||||
throw ZcashError.rustTorLwdGetInfo(
|
||||
lastErrorMessage(fallback: "`TorLwdConn.getInfo` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
defer { zcashlc_free_boxed_slice(infoPtr) }
|
||||
|
||||
let slice = infoPtr.pointee
|
||||
guard let rawPtr = slice.ptr else {
|
||||
throw ZcashError.rustTorLwdGetInfo("`TorLwdConn.getInfo` Null pointer in FfiBoxedSlice")
|
||||
}
|
||||
|
||||
let buffer = UnsafeBufferPointer<UInt8>(start: rawPtr, count: Int(slice.len))
|
||||
let data = Data(buffer: buffer)
|
||||
|
||||
do {
|
||||
let info = try LightdInfo(serializedBytes: data)
|
||||
return info
|
||||
} catch {
|
||||
throw ZcashError.rustTorLwdGetInfo("`TorLwdConn.getInfo` Failed to decode protobuf LightdInfo: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a chain tip of the blockchain (latest block height)
|
||||
/// - Returns: BlockHeight
|
||||
func latestBlockHeight() throws -> BlockHeight {
|
||||
var height: UInt32 = 0
|
||||
|
||||
let blockIDPtr = zcashlc_tor_lwd_conn_latest_block(conn, &height)
|
||||
|
||||
guard let blockIDPtr else {
|
||||
throw ZcashError.rustTorLwdLatestBlockHeight(
|
||||
lastErrorMessage(fallback: "`TorLwdConn.latestBlockHeight` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
defer { zcashlc_free_boxed_slice(blockIDPtr) }
|
||||
|
||||
return BlockHeight(height)
|
||||
}
|
||||
|
||||
/// Gets a tree state for a given height
|
||||
/// - Parameter height: heght for what a tree state is requested
|
||||
/// - Returns: TreeState
|
||||
func getTreeState(height: BlockHeight) throws -> TreeState {
|
||||
let treeStatePtr = zcashlc_tor_lwd_conn_get_tree_state(conn, UInt32(height))
|
||||
|
||||
guard let treeStatePtr else {
|
||||
throw ZcashError.rustTorLwdGetTreeState(
|
||||
lastErrorMessage(fallback: "`TorLwdConn.getTreeState` failed with unknown error")
|
||||
)
|
||||
}
|
||||
|
||||
defer { zcashlc_free_boxed_slice(treeStatePtr) }
|
||||
|
||||
let slice = treeStatePtr.pointee
|
||||
guard let rawPtr = slice.ptr else {
|
||||
throw ZcashError.rustTorLwdGetTreeState("`TorLwdConn.getTreeState` Null pointer in FfiBoxedSlice")
|
||||
}
|
||||
|
||||
let buffer = UnsafeBufferPointer<UInt8>(start: rawPtr, count: Int(slice.len))
|
||||
let data = Data(buffer: buffer)
|
||||
|
||||
do {
|
||||
let treeState = try TreeState(serializedBytes: data)
|
||||
return treeState
|
||||
} catch {
|
||||
throw ZcashError.rustTorLwdGetTreeState("`TorLwdConn.getTreeState` Failed to decode protobuf TreeState: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ class BlockDownloaderTests: XCTestCase {
|
|||
try await super.setUp()
|
||||
testTempDirectory = Environment.uniqueTestTempDirectory
|
||||
|
||||
service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
|
||||
service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default, torURL: nil).make()
|
||||
|
||||
rustBackend = ZcashRustBackend.makeForTests(
|
||||
fsBlockDbRoot: testTempDirectory,
|
||||
|
|
|
@ -197,19 +197,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
|
|||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(0),
|
||||
internalSyncStatus: .syncing(0, false),
|
||||
latestBlockHeight: 0
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(0.9),
|
||||
internalSyncStatus: .syncing(0.9, false),
|
||||
latestBlockHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(1.0),
|
||||
internalSyncStatus: .syncing(1.0, false),
|
||||
latestBlockHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
|
@ -269,19 +269,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
|
|||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(0),
|
||||
internalSyncStatus: .syncing(0, false),
|
||||
latestBlockHeight: 0
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(0.9),
|
||||
internalSyncStatus: .syncing(0.9, false),
|
||||
latestBlockHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[0],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(1.0),
|
||||
internalSyncStatus: .syncing(1.0, false),
|
||||
latestBlockHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
|
@ -320,19 +320,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
|
|||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(0),
|
||||
internalSyncStatus: .syncing(0, false),
|
||||
latestBlockHeight: 663189
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(0.9),
|
||||
internalSyncStatus: .syncing(0.9, false),
|
||||
latestBlockHeight: 663200
|
||||
),
|
||||
SynchronizerState(
|
||||
syncSessionID: uuids[1],
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(1.0),
|
||||
internalSyncStatus: .syncing(1.0, false),
|
||||
latestBlockHeight: 663200
|
||||
),
|
||||
SynchronizerState(
|
||||
|
|
|
@ -97,7 +97,7 @@ class TransactionEnhancementTests: ZcashTestCase {
|
|||
return
|
||||
}
|
||||
|
||||
let service = DarksideWalletService()
|
||||
let service = DarksideWalletService(torURL: nil)
|
||||
darksideWalletService = service
|
||||
|
||||
let storage = FSCompactBlockRepository(
|
||||
|
|
|
@ -52,7 +52,7 @@ class BlockStreamingTest: ZcashTestCase {
|
|||
singleCallTimeoutInMillis: 10000,
|
||||
streamingCallTimeoutInMillis: 10000
|
||||
)
|
||||
let service = LightWalletServiceFactory(endpoint: endpoint).make()
|
||||
let service = LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make()
|
||||
|
||||
latestBlockHeight = try await service.latestBlockHeight()
|
||||
startHeight = latestBlockHeight - 10_000
|
||||
|
@ -77,7 +77,7 @@ class BlockStreamingTest: ZcashTestCase {
|
|||
streamingCallTimeoutInMillis: timeout
|
||||
)
|
||||
self.endpoint = endpoint
|
||||
service = LightWalletServiceFactory(endpoint: endpoint).make()
|
||||
service = LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make()
|
||||
storage = FSCompactBlockRepository(
|
||||
fsBlockDbRoot: testTempDirectory,
|
||||
metadataStore: FSMetadataStore.live(
|
||||
|
@ -97,7 +97,7 @@ class BlockStreamingTest: ZcashTestCase {
|
|||
)
|
||||
|
||||
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in
|
||||
LightWalletServiceFactory(endpoint: endpoint).make()
|
||||
LightWalletServiceFactory(endpoint: endpoint, torURL: nil).make()
|
||||
}
|
||||
|
||||
let transactionRepositoryMock = TransactionRepositoryMock()
|
||||
|
|
|
@ -43,7 +43,7 @@ class CompactBlockProcessorTests: ZcashTestCase {
|
|||
network: ZcashNetworkBuilder.network(for: .testnet)
|
||||
)
|
||||
|
||||
let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make()
|
||||
let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make()
|
||||
let service = MockLightWalletService(
|
||||
latestBlockHeight: mockLatestHeight,
|
||||
service: liveService
|
||||
|
|
|
@ -44,7 +44,7 @@ class CompactBlockReorgTests: ZcashTestCase {
|
|||
network: ZcashNetworkBuilder.network(for: .testnet)
|
||||
)
|
||||
|
||||
let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make()
|
||||
let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make()
|
||||
let service = MockLightWalletService(
|
||||
latestBlockHeight: mockLatestHeight,
|
||||
service: liveService
|
||||
|
|
|
@ -46,7 +46,7 @@ class DownloadTests: ZcashTestCase {
|
|||
ZcashRustBackend.makeForTests(fsBlockDbRoot: self.testTempDirectory, networkType: self.network.networkType)
|
||||
}
|
||||
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in
|
||||
LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make()
|
||||
LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make()
|
||||
}
|
||||
let storage = mockContainer.resolve(CompactBlockRepository.self)
|
||||
try await storage.create()
|
||||
|
|
|
@ -19,7 +19,7 @@ class LightWalletServiceTests: XCTestCase {
|
|||
override func setUp() {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
super.setUp()
|
||||
service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make()
|
||||
service = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet, torURL: nil).make()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
|
|
|
@ -14,23 +14,23 @@ import XCTest
|
|||
class TorClientTests: ZcashTestCase {
|
||||
let network: ZcashNetwork = ZcashNetworkBuilder.network(for: .testnet)
|
||||
|
||||
func testLwdCanFetchAndSubmitTx() async throws {
|
||||
func testLwdCanFetchAndSubmitTx() throws {
|
||||
// Spin up a new Tor client.
|
||||
let client = try await TorClient(torDir: testTempDirectory)
|
||||
let client = try TorClient(torDir: testTempDirectory)
|
||||
|
||||
// Connect to a testnet lightwalletd server.
|
||||
let lwdConn = try await client.connectToLightwalletd(
|
||||
let lwdConn = try client.connectToLightwalletd(
|
||||
endpoint: LightWalletEndpointBuilder.publicTestnet.urlString)
|
||||
|
||||
// Fetch a known testnet transaction.
|
||||
let txId =
|
||||
"9e309d29a99f06e6dcc7aee91dca23c0efc2cf5083cc483463ddbee19c1fadf1"
|
||||
.toTxIdString().hexadecimal!
|
||||
let (tx, status) = try await lwdConn.fetchTransaction(txId: txId)
|
||||
let (tx, status) = try lwdConn.fetchTransaction(txId: txId)
|
||||
XCTAssertEqual(status, .mined(1_234_567))
|
||||
|
||||
// We should fail to resubmit the already-mined transaction.
|
||||
let result = try await lwdConn.submit(spendTransaction: tx!.raw)
|
||||
let result = try lwdConn.submit(spendTransaction: tx!.raw)
|
||||
XCTAssertEqual(result.errorCode, -25)
|
||||
XCTAssertEqual(
|
||||
result.errorMessage,
|
||||
|
|
|
@ -333,7 +333,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
}
|
||||
|
||||
func testIsNewSessionOnUnpreparedToValidTransition() {
|
||||
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(0)))
|
||||
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(0, false)))
|
||||
}
|
||||
|
||||
func testIsNotNewSessionOnUnpreparedToStateThatWontSync() {
|
||||
|
@ -348,12 +348,8 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
func testIsNotNewSyncSessionOnSameSession() {
|
||||
XCTAssertFalse(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.syncing(
|
||||
0.5
|
||||
),
|
||||
.syncing(
|
||||
0.6
|
||||
)
|
||||
.syncing(0.5, false),
|
||||
.syncing(0.6, false)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -362,9 +358,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
XCTAssertTrue(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.synced,
|
||||
.syncing(
|
||||
0.6
|
||||
)
|
||||
.syncing(0.6, false)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -373,9 +367,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
XCTAssertTrue(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.disconnected,
|
||||
.syncing(
|
||||
0.6
|
||||
)
|
||||
.syncing(0.6, false)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -384,16 +376,14 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
XCTAssertTrue(
|
||||
SessionTicker.live.isNewSyncSession(
|
||||
.stopped,
|
||||
.syncing(
|
||||
0.6
|
||||
)
|
||||
.syncing(0.6, false)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testInternalSyncStatusesDontDifferWhenOuterStatusIsTheSame() {
|
||||
XCTAssertFalse(InternalSyncStatus.disconnected.isDifferent(from: .disconnected))
|
||||
XCTAssertFalse(InternalSyncStatus.syncing(0).isDifferent(from: .syncing(0)))
|
||||
XCTAssertFalse(InternalSyncStatus.syncing(0, false).isDifferent(from: .syncing(0, false)))
|
||||
XCTAssertFalse(InternalSyncStatus.stopped.isDifferent(from: .stopped))
|
||||
XCTAssertFalse(InternalSyncStatus.synced.isDifferent(from: .synced))
|
||||
XCTAssertFalse(InternalSyncStatus.unprepared.isDifferent(from: .unprepared))
|
||||
|
@ -402,10 +392,10 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
func testInternalSyncStatusMap_SyncingLowerBound() {
|
||||
let synchronizerState = synchronizerState(
|
||||
for:
|
||||
InternalSyncStatus.syncing(0)
|
||||
InternalSyncStatus.syncing(0, false)
|
||||
)
|
||||
|
||||
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.0, data) {
|
||||
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(0.0, data) {
|
||||
XCTFail("Syncing is expected to be 0% (0.0) but received \(data).")
|
||||
}
|
||||
}
|
||||
|
@ -413,10 +403,10 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
func testInternalSyncStatusMap_SyncingInTheMiddle() {
|
||||
let synchronizerState = synchronizerState(
|
||||
for:
|
||||
InternalSyncStatus.syncing(0.45)
|
||||
InternalSyncStatus.syncing(0.45, false)
|
||||
)
|
||||
|
||||
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.45, data) {
|
||||
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(0.45, data) {
|
||||
XCTFail("Syncing is expected to be 45% (0.45) but received \(data).")
|
||||
}
|
||||
}
|
||||
|
@ -424,18 +414,18 @@ class SynchronizerOfflineTests: ZcashTestCase {
|
|||
func testInternalSyncStatusMap_SyncingUpperBound() {
|
||||
let synchronizerState = synchronizerState(
|
||||
for:
|
||||
InternalSyncStatus.syncing(0.9)
|
||||
InternalSyncStatus.syncing(0.9, false)
|
||||
)
|
||||
|
||||
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.9, data) {
|
||||
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(0.9, data) {
|
||||
XCTFail("Syncing is expected to be 90% (0.9) but received \(data).")
|
||||
}
|
||||
}
|
||||
|
||||
func testInternalSyncStatusMap_FetchingUpperBound() {
|
||||
let synchronizerState = synchronizerState(for: InternalSyncStatus.syncing(1))
|
||||
let synchronizerState = synchronizerState(for: InternalSyncStatus.syncing(1, false))
|
||||
|
||||
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(1.0, data) {
|
||||
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(1.0, data) {
|
||||
XCTFail("Syncing is expected to be 100% (1.0) but received \(data).")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,9 +135,6 @@ class ZcashRustBackendTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testScanProgressThrowsOnWrongValues() {
|
||||
// Assert that throws on Zero denominator
|
||||
XCTAssertThrowsError(try ScanProgress(numerator: 0, denominator: 0).progress())
|
||||
|
||||
// Assert that throws on numerator > denominator
|
||||
XCTAssertThrowsError(try ScanProgress(numerator: 23, denominator: 2).progress())
|
||||
|
||||
|
|
|
@ -50,9 +50,9 @@ class DarksideWalletService: LightWalletService {
|
|||
var service: LightWalletService
|
||||
var darksideService: DarksideStreamerNIOClient
|
||||
|
||||
init(endpoint: LightWalletEndpoint) {
|
||||
init(endpoint: LightWalletEndpoint, torURL: URL?) {
|
||||
self.channel = ChannelProvider().channel(endpoint: endpoint)
|
||||
self.service = LightWalletServiceFactory(endpoint: endpoint).make()
|
||||
self.service = LightWalletServiceFactory(endpoint: endpoint, torURL: torURL).make()
|
||||
self.darksideService = DarksideStreamerNIOClient(channel: channel)
|
||||
}
|
||||
|
||||
|
@ -62,8 +62,8 @@ class DarksideWalletService: LightWalletService {
|
|||
self.service = service
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(endpoint: LightWalletEndpointBuilder.default)
|
||||
convenience init(torURL: URL?) {
|
||||
self.init(endpoint: LightWalletEndpointBuilder.default, torURL: torURL)
|
||||
}
|
||||
|
||||
func blockStream(startHeight: BlockHeight, endHeight: BlockHeight) -> AsyncThrowingStream<ZcashCompactBlock, Error> {
|
||||
|
|
|
@ -2072,6 +2072,26 @@ class SynchronizerMock: Synchronizer {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - estimateBirthdayHeight
|
||||
|
||||
var estimateBirthdayHeightForCallsCount = 0
|
||||
var estimateBirthdayHeightForCalled: Bool {
|
||||
return estimateBirthdayHeightForCallsCount > 0
|
||||
}
|
||||
var estimateBirthdayHeightForReceivedDate: Date?
|
||||
var estimateBirthdayHeightForReturnValue: BlockHeight!
|
||||
var estimateBirthdayHeightForClosure: ((Date) -> BlockHeight)?
|
||||
|
||||
func estimateBirthdayHeight(for date: Date) -> BlockHeight {
|
||||
estimateBirthdayHeightForCallsCount += 1
|
||||
estimateBirthdayHeightForReceivedDate = date
|
||||
if let closure = estimateBirthdayHeightForClosure {
|
||||
return closure(date)
|
||||
} else {
|
||||
return estimateBirthdayHeightForReturnValue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
class TransactionRepositoryMock: TransactionRepository {
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ extension SynchronizerState {
|
|||
SynchronizerState(
|
||||
syncSessionID: .nullID,
|
||||
accountsBalances: [:],
|
||||
internalSyncStatus: .syncing(0),
|
||||
internalSyncStatus: .syncing(0, false),
|
||||
latestBlockHeight: 222222
|
||||
)
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ class TestCoordinator {
|
|||
self.birthday = walletBirthday
|
||||
self.network = network
|
||||
|
||||
let liveService = LightWalletServiceFactory(endpoint: endpoint).make()
|
||||
let liveService = LightWalletServiceFactory(endpoint: endpoint, torURL: databases.torDir).make()
|
||||
self.service = DarksideWalletService(endpoint: endpoint, service: liveService)
|
||||
self.synchronizer = SDKSynchronizer(initializer: initializer)
|
||||
subscribeToState(synchronizer: self.synchronizer)
|
||||
|
|
Loading…
Reference in New Issue