ZcashLightClientKit/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift

865 lines
34 KiB
Swift
Raw Normal View History

//
// SDKSynchronizer.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/6/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
public extension Notification.Name {
/// Notification is posted whenever transactions are updated
///
/// - Important: not yet posted
static let transactionsUpdated = Notification.Name("SDKSyncronizerTransactionUpdated")
/// Posted when the synchronizer is started.
static let synchronizerStarted = Notification.Name("SDKSyncronizerStarted")
/// Posted when there are progress updates.
///
/// - Note: Query userInfo object for NotificationKeys.progress for Float
/// progress percentage and NotificationKeys.blockHeight /// for the current progress height
static let synchronizerProgressUpdated = Notification.Name("SDKSyncronizerProgressUpdated")
2021-06-07 16:00:33 -07:00
static let synchronizerStatusWillUpdate = Notification.Name("SDKSynchronizerStatusWillUpdate")
2021-09-17 06:49:58 -07:00
/// Posted when the synchronizer is synced to latest height
static let synchronizerSynced = Notification.Name("SDKSyncronizerSynced")
/// Posted when the synchronizer is stopped
static let synchronizerStopped = Notification.Name("SDKSyncronizerStopped")
2021-09-17 06:49:58 -07:00
/// Posted when the synchronizer loses connection
static let synchronizerDisconnected = Notification.Name("SDKSyncronizerDisconnected")
2021-09-17 06:49:58 -07:00
/// Posted when the synchronizer starts syncing
static let synchronizerSyncing = Notification.Name("SDKSyncronizerSyncing")
/// Posted when synchronizer starts downloading blocks
2021-06-07 16:00:33 -07:00
static let synchronizerDownloading = Notification.Name("SDKSyncronizerDownloading")
/// Posted when synchronizer starts validating blocks
2021-06-07 16:00:33 -07:00
static let synchronizerValidating = Notification.Name("SDKSyncronizerValidating")
/// Posted when synchronizer starts scanning blocks
2021-06-07 16:00:33 -07:00
static let synchronizerScanning = Notification.Name("SDKSyncronizerScanning")
/// Posted when the synchronizer starts Enhancing
2021-06-07 16:00:33 -07:00
static let synchronizerEnhancing = Notification.Name("SDKSyncronizerEnhancing")
/// Posted when the synchronizer starts fetching UTXOs
2021-06-07 16:00:33 -07:00
static let synchronizerFetching = Notification.Name("SDKSyncronizerFetching")
/// Posted when the synchronizer finds a pendingTransaction that hast been newly mined
/// - Note: query userInfo on NotificationKeys.minedTransaction for the transaction
static let synchronizerMinedTransaction = Notification.Name("synchronizerMinedTransaction")
/// Posted when the synchronizer finds a mined transaction
/// - Note: query userInfo on NotificationKeys.foundTransactions for
/// the `[ConfirmedTransactionEntity]`. This notification could arrive in a background thread.
static let synchronizerFoundTransactions = Notification.Name("synchronizerFoundTransactions")
/// Posted when the synchronizer presents an error
/// - Note: query userInfo on NotificationKeys.error for an error
2019-12-19 05:04:50 -08:00
static let synchronizerFailed = Notification.Name("SDKSynchronizerFailed")
2021-06-14 16:38:05 -07:00
static let synchronizerConnectionStateChanged = Notification.Name("SynchronizerConnectionStateChanged")
}
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
/// Synchronizer implementation for UIKit and iOS 13+
2021-09-15 05:21:29 -07:00
// swiftlint:disable type_body_length
public class SDKSynchronizer: Synchronizer {
public struct SynchronizerState {
public var shieldedBalance: WalletBalance
public var transparentBalance: WalletBalance
public var syncStatus: SyncStatus
public var latestScannedHeight: BlockHeight
}
2021-09-15 05:21:29 -07:00
public enum NotificationKeys {
public static let progress = "SDKSynchronizer.progress"
public static let blockHeight = "SDKSynchronizer.blockHeight"
public static let blockDate = "SDKSynchronizer.blockDate"
public static let minedTransaction = "SDKSynchronizer.minedTransaction"
public static let foundTransactions = "SDKSynchronizer.foundTransactions"
2019-12-19 05:04:50 -08:00
public static let error = "SDKSynchronizer.error"
2021-06-07 16:00:33 -07:00
public static let currentStatus = "SDKSynchronizer.currentStatus"
public static let nextStatus = "SDKSynchronizer.nextStatus"
2021-06-14 16:38:05 -07:00
public static let currentConnectionState = "SDKSynchronizer.currentConnectionState"
public static let previousConnectionState = "SDKSynchronizer.previousConnectionState"
public static let synchronizerState = "SDKSynchronizer.synchronizerState"
}
2021-06-14 16:38:05 -07:00
public private(set) var status: SyncStatus {
didSet {
notify(status: status)
}
2021-06-07 16:00:33 -07:00
willSet {
notifyStatusChange(newValue: newValue, oldValue: status)
}
}
public private(set) var progress: Float = 0.0
public private(set) var blockProcessor: CompactBlockProcessor
public private(set) var initializer: Initializer
2021-07-26 16:22:30 -07:00
public private(set) var latestScannedHeight: BlockHeight
2021-06-15 14:53:21 -07:00
public private(set) var connectionState: ConnectionState
2021-07-28 09:59:10 -07:00
public private(set) var network: ZcashNetwork
private var transactionManager: OutboundTransactionManager
private var transactionRepository: TransactionRepository
2020-12-11 12:15:29 -08:00
private var utxoRepository: UnspentTransactionOutputRepository
2021-09-17 06:49:58 -07:00
/// Creates an SDKSynchronizer instance
/// - Parameter initializer: a wallet Initializer object
public convenience init(initializer: Initializer) throws {
2021-09-15 05:21:29 -07:00
try self.init(
2021-09-17 06:49:58 -07:00
status: .unprepared,
initializer: initializer,
transactionManager: try OutboundTransactionManagerBuilder.build(initializer: initializer),
transactionRepository: initializer.transactionRepository,
utxoRepository: try UTXORepositoryBuilder.build(initializer: initializer),
blockProcessor: CompactBlockProcessor(initializer: initializer)
)
2021-09-15 05:21:29 -07:00
}
init(
status: SyncStatus,
initializer: Initializer,
transactionManager: OutboundTransactionManager,
transactionRepository: TransactionRepository,
utxoRepository: UnspentTransactionOutputRepository,
blockProcessor: CompactBlockProcessor
) throws {
2021-06-15 14:53:21 -07:00
self.connectionState = .idle
self.status = status
self.initializer = initializer
self.transactionManager = transactionManager
self.transactionRepository = transactionRepository
2020-12-11 12:15:29 -08:00
self.utxoRepository = utxoRepository
self.blockProcessor = blockProcessor
self.latestScannedHeight = (try? transactionRepository.lastScannedHeight()) ?? initializer.walletBirthday
2021-07-26 16:22:30 -07:00
self.network = initializer.network
self.subscribeToProcessorNotifications(blockProcessor)
}
deinit {
NotificationCenter.default.removeObserver(self)
Task { [blockProcessor] in
await blockProcessor.stop()
}
}
public func prepare(with seed: [UInt8]?) async throws -> Initializer.InitializationResult {
if case .seedRequired = try self.initializer.initialize(with: seed) {
return .seedRequired
}
try await self.blockProcessor.setStartHeight(initializer.walletBirthday)
2021-05-05 12:08:57 -07:00
self.status = .disconnected
return .success
2021-05-05 12:08:57 -07:00
}
2021-09-17 06:49:58 -07:00
/// Starts the synchronizer
/// - Throws: CompactBlockProcessorError when failures occur
public func start(retry: Bool = false) throws {
2021-05-05 12:08:57 -07:00
switch status {
case .unprepared:
throw SynchronizerError.notPrepared
2021-09-17 06:49:58 -07:00
case .downloading, .validating, .scanning, .enhancing, .fetching:
2021-07-15 10:43:42 -07:00
LoggerProxy.warn("warning: synchronizer started when already started")
return
2021-09-17 06:49:58 -07:00
2021-09-15 05:21:29 -07:00
case .stopped, .synced, .disconnected, .error:
Task {
await blockProcessor.start(retry: retry)
2021-05-05 12:08:57 -07:00
}
}
}
/// Stops the synchronizer
public func stop() {
2021-05-05 12:08:57 -07:00
guard status != .stopped, status != .disconnected else {
LoggerProxy.info("attempted to stop when status was: \(status)")
return
}
Task(priority: .high) {
await blockProcessor.stop()
self.status = .stopped
}
}
private func subscribeToProcessorNotifications(_ processor: CompactBlockProcessor) {
let center = NotificationCenter.default
2021-09-15 05:21:29 -07:00
center.addObserver(
self,
selector: #selector(processorUpdated(_:)),
name: Notification.Name.blockProcessorUpdated,
object: processor
)
center.addObserver(
self,
selector: #selector(processorStartedDownloading(_:)),
name: Notification.Name.blockProcessorStartedDownloading,
object: processor
)
center.addObserver(
self,
selector: #selector(processorStartedValidating(_:)),
name: Notification.Name.blockProcessorStartedValidating,
object: processor
)
center.addObserver(
self,
selector: #selector(processorStartedScanning(_:)),
name: Notification.Name.blockProcessorStartedScanning,
object: processor
)
center.addObserver(
self,
selector: #selector(processorStartedEnhancing(_:)),
name: Notification.Name.blockProcessorStartedEnhancing,
object: processor
)
center.addObserver(
self,
selector: #selector(processorStartedFetching(_:)),
name: Notification.Name.blockProcessorStartedFetching,
object: processor
)
center.addObserver(
self,
selector: #selector(processorStopped(_:)),
name: Notification.Name.blockProcessorStopped,
object: processor
)
center.addObserver(
self,
selector: #selector(processorFailed(_:)),
name: Notification.Name.blockProcessorFailed,
object: processor
)
2021-09-15 05:21:29 -07:00
center.addObserver(
self,
selector: #selector(processorFinished(_:)),
name: Notification.Name.blockProcessorFinished,
object: processor
)
center.addObserver(
self,
selector: #selector(processorTransitionUnknown(_:)),
name: Notification.Name.blockProcessorUnknownTransition,
object: processor
)
center.addObserver(
self,
selector: #selector(reorgDetected(_:)),
name: Notification.Name.blockProcessorHandledReOrg,
object: processor
)
center.addObserver(
self,
selector: #selector(transactionsFound(_:)),
name: Notification.Name.blockProcessorFoundTransactions,
object: processor
)
center.addObserver(
self,
selector: #selector(connectivityStateChanged(_:)),
name: Notification.Name.blockProcessorConnectivityStateChanged,
object: nil
)
}
// MARK: Block Processor notifications
2021-09-17 06:49:58 -07:00
2021-06-14 16:38:05 -07:00
@objc func connectivityStateChanged(_ notification: Notification) {
2021-09-17 06:49:58 -07:00
guard
let userInfo = notification.userInfo,
let previous = userInfo[CompactBlockProcessorNotificationKey.previousConnectivityStatus] as? ConnectivityState,
let current = userInfo[CompactBlockProcessorNotificationKey.currentConnectivityStatus] as? ConnectivityState
else {
LoggerProxy.error(
"Found \(Notification.Name.blockProcessorConnectivityStateChanged) but lacks dictionary information." +
"This is probably a programming error"
)
2021-06-14 16:38:05 -07:00
return
}
2021-09-17 06:49:58 -07:00
2021-06-15 14:53:21 -07:00
let currentState = ConnectionState(current)
NotificationCenter.default.mainThreadPost(
2021-06-14 16:38:05 -07:00
name: .synchronizerConnectionStateChanged,
object: self,
userInfo: [
2021-09-15 05:21:29 -07:00
NotificationKeys.previousConnectionState: ConnectionState(previous),
NotificationKeys.currentConnectionState: currentState
]
)
2021-06-14 16:38:05 -07:00
2021-06-15 14:53:21 -07:00
DispatchQueue.main.async { [weak self] in
self?.connectionState = currentState
}
2021-06-14 16:38:05 -07:00
}
@objc func transactionsFound(_ notification: Notification) {
2021-09-17 06:49:58 -07:00
guard
let userInfo = notification.userInfo,
let foundTransactions = userInfo[CompactBlockProcessorNotificationKey.foundTransactions] as? [ConfirmedTransactionEntity]
else {
return
}
2021-09-17 06:49:58 -07:00
NotificationCenter.default.mainThreadPost(
2021-09-15 05:21:29 -07:00
name: .synchronizerFoundTransactions,
object: self,
userInfo: [
NotificationKeys.foundTransactions: foundTransactions
]
)
}
@objc func reorgDetected(_ notification: Notification) {
2021-09-17 06:49:58 -07:00
guard
let userInfo = notification.userInfo,
let progress = userInfo[CompactBlockProcessorNotificationKey.reorgHeight] as? BlockHeight,
2021-09-17 06:49:58 -07:00
let rewindHeight = userInfo[CompactBlockProcessorNotificationKey.rewindHeight] as? BlockHeight
else {
LoggerProxy.debug("error processing reorg notification")
return
}
LoggerProxy.debug("handling reorg at: \(progress) with rewind height: \(rewindHeight)")
2021-09-17 06:49:58 -07:00
do {
try transactionManager.handleReorg(at: rewindHeight)
} catch {
LoggerProxy.debug("error handling reorg: \(error)")
2019-12-19 05:04:50 -08:00
notifyFailure(error)
}
}
@objc func processorUpdated(_ notification: Notification) {
2021-09-17 06:49:58 -07:00
guard
let userInfo = notification.userInfo,
let progress = userInfo[CompactBlockProcessorNotificationKey.progress] as? CompactBlockProgress
else {
return
}
2021-06-07 16:00:33 -07:00
self.notify(progress: progress)
}
@objc func processorStartedDownloading(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
2021-09-17 06:49:58 -07:00
guard let self = self, self.status != .downloading(.nullProgress) else { return }
self.status = .downloading(.nullProgress)
}
}
@objc func processorStartedValidating(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
2021-06-14 16:38:05 -07:00
guard let self = self, self.status != .validating else { return }
2021-06-07 16:00:33 -07:00
self.status = .validating
}
}
@objc func processorStartedScanning(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
2021-09-17 06:49:58 -07:00
guard let self = self, self.status != .scanning(.nullProgress) else { return }
self.status = .scanning(.nullProgress)
2021-06-07 16:00:33 -07:00
}
}
@objc func processorStartedEnhancing(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
2021-06-14 16:38:05 -07:00
guard let self = self, self.status != .enhancing(NullEnhancementProgress()) else { return }
self.status = .enhancing(NullEnhancementProgress())
2021-06-07 16:00:33 -07:00
}
}
@objc func processorStartedFetching(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
2021-06-14 16:38:05 -07:00
guard let self = self, self.status != .fetching else { return }
2021-06-07 16:00:33 -07:00
self.status = .fetching
}
}
@objc func processorStopped(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
2021-06-14 16:38:05 -07:00
guard let self = self, self.status != .stopped else { return }
self.status = .stopped
}
}
@objc func processorFailed(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if let error = notification.userInfo?[CompactBlockProcessorNotificationKey.error] as? Error {
self.notifyFailure(error)
2021-06-14 16:38:05 -07:00
self.status = .error(self.mapError(error))
} else {
2021-09-15 05:21:29 -07:00
self.notifyFailure(
CompactBlockProcessorError.generalError(
2021-09-17 06:49:58 -07:00
message: "This is strange. processorFailed Call received no error message"
)
2021-09-15 05:21:29 -07:00
)
2021-06-14 16:38:05 -07:00
self.status = .error(SynchronizerError.generalError(message: "This is strange. processorFailed Call received no error message"))
}
}
}
@objc func processorFinished(_ notification: Notification) {
// FIX: Pending transaction updates fail if done from another thread. Improvement needed: explicitly define queues for sql repositories see: https://github.com/zcash/ZcashLightClientKit/issues/450
if let blockHeight = notification.userInfo?[CompactBlockProcessorNotificationKey.latestScannedBlockHeight] as? BlockHeight {
self.latestScannedHeight = blockHeight
}
self.refreshPendingTransactions()
self.status = .synced
}
@objc func processorTransitionUnknown(_ notification: Notification) {
self.status = .disconnected
}
// MARK: Synchronizer methods
public func sendToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
toAddress: Recipient,
memo: Memo?
) async throws -> PendingTransactionEntity {
do {
try await initializer.downloadParametersIfNeeded()
} catch {
throw SynchronizerError.parameterMissing(underlyingError: error)
2020-10-08 10:00:27 -07:00
}
if case Recipient.transparent = toAddress,
memo != nil {
throw SynchronizerError.generalError(message: "Memos can't be sent to transparent addresses.")
}
return try await createToAddress(
spendingKey: spendingKey,
zatoshi: zatoshi,
recipient: toAddress,
memo: memo
)
}
2020-10-08 10:00:27 -07:00
2021-09-15 05:21:29 -07:00
public func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo
) async throws -> PendingTransactionEntity {
2020-12-23 15:01:09 -08:00
// let's see if there are funds to shield
let accountIndex = Int(spendingKey.account)
2020-12-23 15:01:09 -08:00
do {
let tBalance = try await self.getTransparentBalance(accountIndex: accountIndex)
2020-12-23 15:01:09 -08:00
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
// Verify that at least there are funds for the fee. Ideally this logic will be improved by the shielding wallet.
2021-07-26 16:22:30 -07:00
guard tBalance.verified >= self.network.constants.defaultFee(for: self.latestScannedHeight) else {
throw ShieldFundsError.insuficientTransparentFunds
2020-12-23 15:01:09 -08:00
}
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, recipient: .internalAccount(spendingKey.account), memo: try memo.asMemoBytes(), from: accountIndex)
2020-12-23 15:01:09 -08:00
// TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487
let transaction = try await transactionManager.encodeShieldingTransaction(
spendingKey: spendingKey,
pendingTransaction: shieldingSpend
)
return try await transactionManager.submit(pendingTransaction: transaction)
2020-12-23 15:01:09 -08:00
} catch {
throw error
2020-12-23 15:01:09 -08:00
}
}
2021-09-17 06:49:58 -07:00
func createToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
recipient: Recipient,
memo: Memo?
) async throws -> PendingTransactionEntity {
do {
2021-09-17 06:49:58 -07:00
let spend = try transactionManager.initSpend(
zatoshi: zatoshi,
recipient: .address(recipient),
memo: memo?.asMemoBytes(),
from: Int(spendingKey.account)
2021-09-17 06:49:58 -07:00
)
let transaction = try await transactionManager.encode(
spendingKey: spendingKey,
pendingTransaction: spend
)
let submittedTx = try await transactionManager.submit(pendingTransaction: transaction)
return submittedTx
} catch {
throw error
}
}
public func cancelSpend(transaction: PendingTransactionEntity) -> Bool {
transactionManager.cancel(pendingTransaction: transaction)
}
public func allReceivedTransactions() throws -> [ConfirmedTransactionEntity] {
try transactionRepository.findAllReceivedTransactions(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
}
public func allPendingTransactions() throws -> [PendingTransactionEntity] {
try transactionManager.allPendingTransactions() ?? [PendingTransactionEntity]()
}
public func allClearedTransactions() throws -> [ConfirmedTransactionEntity] {
try transactionRepository.findAll(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
}
public func allSentTransactions() throws -> [ConfirmedTransactionEntity] {
try transactionRepository.findAllSentTransactions(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
}
public func allConfirmedTransactions(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? {
try transactionRepository.findAll(from: transaction, limit: limit)
}
public func paginatedTransactions(of kind: TransactionKind = .all) -> PaginatedTransactionRepository {
PagedTransactionRepositoryBuilder.build(initializer: initializer, kind: .all)
}
public func latestDownloadedHeight() async throws -> BlockHeight {
try await blockProcessor.downloader.lastDownloadedBlockHeight()
2020-10-06 16:35:17 -07:00
}
public func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) {
Task {
do {
let latestBlockHeight = try await blockProcessor.downloader.latestBlockHeightAsync()
result(.success(latestBlockHeight))
} catch {
result(.failure(error))
}
}
2020-10-06 16:35:17 -07:00
}
public func latestHeight() async throws -> BlockHeight {
try await blockProcessor.downloader.latestBlockHeight()
2020-10-06 16:35:17 -07:00
}
public func latestUTXOs(address: String) async throws -> [UnspentTransactionOutputEntity] {
2020-12-11 12:15:29 -08:00
guard initializer.isValidTransparentAddress(address) else {
throw SynchronizerError.generalError(message: "invalid t-address")
2020-12-11 12:15:29 -08:00
}
let stream = initializer.lightWalletService.fetchUTXOs(for: address, height: network.constants.saplingActivationHeight)
do {
var utxos: [UnspentTransactionOutputEntity] = []
for try await transactionEntity in stream {
utxos.append(transactionEntity)
2020-12-11 12:15:29 -08:00
}
try self.utxoRepository.clearAll(address: address)
try self.utxoRepository.store(utxos: utxos)
return utxos
} catch {
throw SynchronizerError.generalError(message: "\(error)")
2021-09-17 06:49:58 -07:00
}
2020-12-11 12:15:29 -08:00
}
2021-04-01 07:27:26 -07:00
public func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) async throws -> RefreshedUTXOs {
[476] CompactBlockProcessor to async/await - getting rid of the Operation Queue - the cleanup is needed - the update of tests is needed - tested and it successfully finishes the sync process [476] CompactBlockProcessor to async/await - old processNewBlocks() removed [476] CompactBlockProcessor to async/await - unused operations removed [476] CompactBlockProcessor to async/await - unit tests update [476] CompactBlockProcessor to async/await - unit tests refactored [476] CompactBlockProcessor to async/await - cleanup of deprecated method [476] CompactBlockProcessor to async/await - fail(error) was called even for canceled tasks but that must be excluded [476] CompactBlockProcessor to async/await - removal of all ZcashOperations from the code (unit test will follow) [476] CompactBlockProcessor to async/await - network tests in building and success order again [476] CompactBlockProcessor to async/await - offline tests in building and success order [476] CompactBlockProcessor to async/await (519) - cleanup of suspending the task [476] CompactBlockProcessor to async/await (519) - most comments resolved [476] CompactBlockProcessor to async/await (519) - thread safe state for both sync and async context [476] CompactBlockProcessor to async/await (519) - fixed build for a sample project [476] CompactBlockProcessor to async/await (519) - func testStartNotifiesSuscriptors() reverted [476] CompactBlockProcessor to async/await (519) - TODO added to track why we used NSLock instead of an Actor - Task priority enhanced [476] CompactBlockProcessor to async/await (519) - cleanup in Tasks and priorities
2022-09-01 05:58:41 -07:00
try await blockProcessor.refreshUTXOs(tAddress: address, startHeight: height)
}
@available(*, deprecated, message: "This function will be removed soon, use the one returning a `Zatoshi` value instead")
public func getShieldedBalance(accountIndex: Int = 0) -> Int64 {
initializer.getBalance(account: accountIndex).amount
}
public func getShieldedBalance(accountIndex: Int = 0) -> Zatoshi {
initializer.getBalance(account: accountIndex)
}
@available(*, deprecated, message: "This function will be removed soon, use the one returning a `Zatoshi` value instead")
public func getShieldedVerifiedBalance(accountIndex: Int = 0) -> Int64 {
initializer.getVerifiedBalance(account: accountIndex).amount
}
public func getShieldedVerifiedBalance(accountIndex: Int = 0) -> Zatoshi {
initializer.getVerifiedBalance(account: accountIndex)
}
public func getSaplingAddress(accountIndex: Int) async -> SaplingAddress? {
await blockProcessor.getSaplingAddress(accountIndex: accountIndex)
2021-04-08 10:18:16 -07:00
}
public func getUnifiedAddress(accountIndex: Int) async -> UnifiedAddress? {
await blockProcessor.getUnifiedAddress(accountIndex: accountIndex)
2021-04-08 10:18:16 -07:00
}
public func getTransparentAddress(accountIndex: Int) async -> TransparentAddress? {
await blockProcessor.getTransparentAddress(accountIndex: accountIndex)
2021-04-08 10:18:16 -07:00
}
/// Returns the last stored transparent balance
public func getTransparentBalance(accountIndex: Int) async throws -> WalletBalance {
2022-10-31 05:57:10 -07:00
try await blockProcessor.getTransparentBalance(accountIndex: accountIndex)
2021-04-08 10:18:16 -07:00
}
2022-10-31 05:57:10 -07:00
public func rewind(_ policy: RewindPolicy) async throws {
self.stop()
var height: BlockHeight?
switch policy {
case .quick:
break
2021-09-17 06:49:58 -07:00
case .birthday:
let birthday = await self.blockProcessor.config.walletBirthday
height = birthday
case .height(let rewindHeight):
height = rewindHeight
2021-09-17 06:49:58 -07:00
case .transaction(let transaction):
guard let txHeight = transaction.anchor(network: self.network) else {
throw SynchronizerError.rewindErrorUnknownArchorHeight
}
height = txHeight
}
do {
let rewindHeight = try await self.blockProcessor.rewindTo(height)
try self.transactionManager.handleReorg(at: rewindHeight)
} catch {
throw SynchronizerError.rewindError(underlyingError: error)
}
}
// MARK: notify state
2021-06-07 16:00:33 -07:00
private func notify(progress: CompactBlockProgress) {
2021-09-17 06:49:58 -07:00
var userInfo: [AnyHashable: Any] = .init()
userInfo[NotificationKeys.progress] = progress
2021-06-07 16:00:33 -07:00
userInfo[NotificationKeys.blockHeight] = progress.progressHeight
2021-07-07 07:42:45 -07:00
self.status = SyncStatus(progress)
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerProgressUpdated, object: self, userInfo: userInfo)
}
2021-06-14 16:38:05 -07:00
private func notifyStatusChange(newValue: SyncStatus, oldValue: SyncStatus) {
NotificationCenter.default.mainThreadPost(
2021-09-17 06:49:58 -07:00
name: .synchronizerStatusWillUpdate,
object: self,
userInfo:
[
NotificationKeys.currentStatus: oldValue,
NotificationKeys.nextStatus: newValue
]
)
2021-06-07 16:00:33 -07:00
}
2021-06-14 16:38:05 -07:00
private func notify(status: SyncStatus) {
switch status {
case .disconnected:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerDisconnected, object: self)
case .stopped:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerStopped, object: self)
case .synced:
Task {
NotificationCenter.default.mainThreadPost(
name: Notification.Name.synchronizerSynced,
object: self,
userInfo: [
SDKSynchronizer.NotificationKeys.blockHeight: self.latestScannedHeight,
SDKSynchronizer.NotificationKeys.synchronizerState: SynchronizerState(
shieldedBalance: WalletBalance(
verified: initializer.getVerifiedBalance(),
total: initializer.getBalance()
),
transparentBalance: (try? await self.getTransparentBalance(accountIndex: 0)) ?? WalletBalance.zero,
syncStatus: status,
latestScannedHeight: self.latestScannedHeight
)
]
)
}
2021-05-05 12:08:57 -07:00
case .unprepared:
break
2021-06-07 16:00:33 -07:00
case .downloading:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerDownloading, object: self)
2021-06-07 16:00:33 -07:00
case .validating:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerValidating, object: self)
2021-06-07 16:00:33 -07:00
case .scanning:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerScanning, object: self)
2021-06-07 16:00:33 -07:00
case .enhancing:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerEnhancing, object: self)
2021-06-07 16:00:33 -07:00
case .fetching:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerFetching, object: self)
2021-06-14 16:38:05 -07:00
case .error(let e):
self.notifyFailure(e)
}
}
// MARK: book keeping
private func updateMinedTransactions() throws {
2021-09-17 06:49:58 -07:00
try transactionManager.allPendingTransactions()?
.filter { $0.isSubmitSuccess && !$0.isMined }
.forEach { pendingTx in
guard let rawId = pendingTx.rawTransactionId else { return }
let transaction = try transactionRepository.findBy(rawId: rawId)
guard let minedHeight = transaction?.minedHeight else { return }
let minedTx = try transactionManager.applyMinedHeight(pendingTransaction: pendingTx, minedHeight: minedHeight)
notifyMinedTransaction(minedTx)
}
}
private func removeConfirmedTransactions() throws {
let latestHeight = try transactionRepository.lastScannedHeight()
2021-09-17 06:49:58 -07:00
try transactionManager.allPendingTransactions()?
.filter { $0.minedHeight > 0 && abs($0.minedHeight - latestHeight) >= ZcashSDK.defaultStaleTolerance }
.forEach { try transactionManager.delete(pendingTransaction: $0) }
}
private func refreshPendingTransactions() {
do {
try updateMinedTransactions()
try removeConfirmedTransactions()
} catch {
LoggerProxy.debug("error refreshing pending transactions: \(error)")
}
}
2021-09-17 06:49:58 -07:00
private func notifyMinedTransaction(_ transaction: PendingTransactionEntity) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
NotificationCenter.default.mainThreadPost(
2021-09-17 06:49:58 -07:00
name: Notification.Name.synchronizerMinedTransaction,
object: self,
userInfo: [NotificationKeys.minedTransaction: transaction]
)
}
}
2021-09-17 06:49:58 -07:00
// swiftlint:disable cyclomatic_complexity
private func mapError(_ error: Error) -> Error {
2021-09-17 06:49:58 -07:00
if let compactBlockProcessorError = error as? CompactBlockProcessorError {
switch compactBlockProcessorError {
case .dataDbInitFailed(let path):
return SynchronizerError.initFailed(message: "DataDb init failed at path: \(path)")
case .connectionError(let message):
return SynchronizerError.connectionFailed(message: message)
case .invalidConfiguration:
return SynchronizerError.generalError(message: "Invalid Configuration")
case .missingDbPath(let path):
return SynchronizerError.initFailed(message: "missing Db path: \(path)")
case .generalError(let message):
return SynchronizerError.generalError(message: message)
case .maxAttemptsReached(attempts: let attempts):
return SynchronizerError.maxRetryAttemptsReached(attempts: attempts)
2021-09-17 06:49:58 -07:00
case let .grpcError(statusCode, message):
return SynchronizerError.connectionError(status: statusCode, message: message)
case .connectionTimeout:
return SynchronizerError.networkTimeout
case .unspecifiedError(let underlyingError):
return SynchronizerError.uncategorized(underlyingError: underlyingError)
case .criticalError:
return SynchronizerError.criticalError
2021-04-08 10:18:16 -07:00
case .invalidAccount:
return SynchronizerError.invalidAccount
2021-05-18 07:49:57 -07:00
case .wrongConsensusBranchId:
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
2021-05-18 07:49:57 -07:00
case .networkMismatch:
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
2021-05-18 07:49:57 -07:00
case .saplingActivationMismatch:
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
[476] CompactBlockProcessor to async/await - getting rid of the Operation Queue - the cleanup is needed - the update of tests is needed - tested and it successfully finishes the sync process [476] CompactBlockProcessor to async/await - old processNewBlocks() removed [476] CompactBlockProcessor to async/await - unused operations removed [476] CompactBlockProcessor to async/await - unit tests update [476] CompactBlockProcessor to async/await - unit tests refactored [476] CompactBlockProcessor to async/await - cleanup of deprecated method [476] CompactBlockProcessor to async/await - fail(error) was called even for canceled tasks but that must be excluded [476] CompactBlockProcessor to async/await - removal of all ZcashOperations from the code (unit test will follow) [476] CompactBlockProcessor to async/await - network tests in building and success order again [476] CompactBlockProcessor to async/await - offline tests in building and success order [476] CompactBlockProcessor to async/await (519) - cleanup of suspending the task [476] CompactBlockProcessor to async/await (519) - most comments resolved [476] CompactBlockProcessor to async/await (519) - thread safe state for both sync and async context [476] CompactBlockProcessor to async/await (519) - fixed build for a sample project [476] CompactBlockProcessor to async/await (519) - func testStartNotifiesSuscriptors() reverted [476] CompactBlockProcessor to async/await (519) - TODO added to track why we used NSLock instead of an Actor - Task priority enhanced [476] CompactBlockProcessor to async/await (519) - cleanup in Tasks and priorities
2022-09-01 05:58:41 -07:00
case .unknown: break
}
}
2021-09-17 06:49:58 -07:00
return SynchronizerError.uncategorized(underlyingError: error)
}
2019-12-19 05:04:50 -08:00
private func notifyFailure(_ error: Error) {
2021-09-17 06:49:58 -07:00
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
NotificationCenter.default.mainThreadPost(
2021-09-17 06:49:58 -07:00
name: Notification.Name.synchronizerFailed,
object: self,
userInfo: [NotificationKeys.error: self.mapError(error)]
)
2019-12-19 05:04:50 -08:00
}
}
}
extension SDKSynchronizer {
public var pendingTransactions: [PendingTransactionEntity] {
(try? self.allPendingTransactions()) ?? [PendingTransactionEntity]()
}
public var clearedTransactions: [ConfirmedTransactionEntity] {
(try? self.allClearedTransactions()) ?? [ConfirmedTransactionEntity]()
}
public var sentTransactions: [ConfirmedTransactionEntity] {
(try? self.allSentTransactions()) ?? [ConfirmedTransactionEntity]()
}
public var receivedTransactions: [ConfirmedTransactionEntity] {
(try? self.allReceivedTransactions()) ?? [ConfirmedTransactionEntity]()
}
}
2021-06-14 16:38:05 -07:00
import GRPC
extension ConnectionState {
init(_ connectivityState: ConnectivityState) {
switch connectivityState {
case .connecting:
self = .connecting
case .idle:
self = .idle
case .ready:
self = .online
case .shutdown:
self = .shutdown
case .transientFailure:
self = .reconnecting
}
}
}
2021-09-17 06:49:58 -07:00
private struct NullEnhancementProgress: EnhancementProgress {
2021-06-14 16:38:05 -07:00
var totalTransactions: Int { 0 }
var enhancedTransactions: Int { 0 }
var lastFoundTransaction: ConfirmedTransactionEntity? { nil }
var range: CompactBlockRange { 0 ... 0 }
}