This commit is contained in:
Lukas Korba 2025-04-16 07:58:24 +00:00 committed by GitHub
commit afc33d8f64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 522 additions and 188 deletions

View File

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

View File

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

View File

@ -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] = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -97,7 +97,7 @@ class TransactionEnhancementTests: ZcashTestCase {
return
}
let service = DarksideWalletService()
let service = DarksideWalletService(torURL: nil)
darksideWalletService = service
let storage = FSCompactBlockRepository(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -146,7 +146,7 @@ extension SynchronizerState {
SynchronizerState(
syncSessionID: .nullID,
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, false),
latestBlockHeight: 222222
)
}

View File

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