ZcashLightClientKit/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift

865 lines
34 KiB
Swift

//
// 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")
static let synchronizerStatusWillUpdate = Notification.Name("SDKSynchronizerStatusWillUpdate")
/// 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")
/// Posted when the synchronizer loses connection
static let synchronizerDisconnected = Notification.Name("SDKSyncronizerDisconnected")
/// Posted when the synchronizer starts syncing
static let synchronizerSyncing = Notification.Name("SDKSyncronizerSyncing")
/// Posted when synchronizer starts downloading blocks
static let synchronizerDownloading = Notification.Name("SDKSyncronizerDownloading")
/// Posted when synchronizer starts validating blocks
static let synchronizerValidating = Notification.Name("SDKSyncronizerValidating")
/// Posted when synchronizer starts scanning blocks
static let synchronizerScanning = Notification.Name("SDKSyncronizerScanning")
/// Posted when the synchronizer starts Enhancing
static let synchronizerEnhancing = Notification.Name("SDKSyncronizerEnhancing")
/// Posted when the synchronizer starts fetching UTXOs
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
static let synchronizerFailed = Notification.Name("SDKSynchronizerFailed")
static let synchronizerConnectionStateChanged = Notification.Name("SynchronizerConnectionStateChanged")
}
/// Synchronizer implementation for UIKit and iOS 13+
// 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
}
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"
public static let error = "SDKSynchronizer.error"
public static let currentStatus = "SDKSynchronizer.currentStatus"
public static let nextStatus = "SDKSynchronizer.nextStatus"
public static let currentConnectionState = "SDKSynchronizer.currentConnectionState"
public static let previousConnectionState = "SDKSynchronizer.previousConnectionState"
public static let synchronizerState = "SDKSynchronizer.synchronizerState"
}
public private(set) var status: SyncStatus {
didSet {
notify(status: status)
}
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
public private(set) var latestScannedHeight: BlockHeight
public private(set) var connectionState: ConnectionState
public private(set) var network: ZcashNetwork
private var transactionManager: OutboundTransactionManager
private var transactionRepository: TransactionRepository
private var utxoRepository: UnspentTransactionOutputRepository
/// Creates an SDKSynchronizer instance
/// - Parameter initializer: a wallet Initializer object
public convenience init(initializer: Initializer) throws {
try self.init(
status: .unprepared,
initializer: initializer,
transactionManager: try OutboundTransactionManagerBuilder.build(initializer: initializer),
transactionRepository: initializer.transactionRepository,
utxoRepository: try UTXORepositoryBuilder.build(initializer: initializer),
blockProcessor: CompactBlockProcessor(initializer: initializer)
)
}
init(
status: SyncStatus,
initializer: Initializer,
transactionManager: OutboundTransactionManager,
transactionRepository: TransactionRepository,
utxoRepository: UnspentTransactionOutputRepository,
blockProcessor: CompactBlockProcessor
) throws {
self.connectionState = .idle
self.status = status
self.initializer = initializer
self.transactionManager = transactionManager
self.transactionRepository = transactionRepository
self.utxoRepository = utxoRepository
self.blockProcessor = blockProcessor
self.latestScannedHeight = (try? transactionRepository.lastScannedHeight()) ?? initializer.walletBirthday
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)
self.status = .disconnected
return .success
}
/// Starts the synchronizer
/// - Throws: CompactBlockProcessorError when failures occur
public func start(retry: Bool = false) throws {
switch status {
case .unprepared:
throw SynchronizerError.notPrepared
case .downloading, .validating, .scanning, .enhancing, .fetching:
LoggerProxy.warn("warning: synchronizer started when already started")
return
case .stopped, .synced, .disconnected, .error:
Task {
await blockProcessor.start(retry: retry)
}
}
}
/// Stops the synchronizer
public func stop() {
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
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
)
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
@objc func connectivityStateChanged(_ notification: Notification) {
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"
)
return
}
let currentState = ConnectionState(current)
NotificationCenter.default.mainThreadPost(
name: .synchronizerConnectionStateChanged,
object: self,
userInfo: [
NotificationKeys.previousConnectionState: ConnectionState(previous),
NotificationKeys.currentConnectionState: currentState
]
)
DispatchQueue.main.async { [weak self] in
self?.connectionState = currentState
}
}
@objc func transactionsFound(_ notification: Notification) {
guard
let userInfo = notification.userInfo,
let foundTransactions = userInfo[CompactBlockProcessorNotificationKey.foundTransactions] as? [ConfirmedTransactionEntity]
else {
return
}
NotificationCenter.default.mainThreadPost(
name: .synchronizerFoundTransactions,
object: self,
userInfo: [
NotificationKeys.foundTransactions: foundTransactions
]
)
}
@objc func reorgDetected(_ notification: Notification) {
guard
let userInfo = notification.userInfo,
let progress = userInfo[CompactBlockProcessorNotificationKey.reorgHeight] as? BlockHeight,
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)")
do {
try transactionManager.handleReorg(at: rewindHeight)
} catch {
LoggerProxy.debug("error handling reorg: \(error)")
notifyFailure(error)
}
}
@objc func processorUpdated(_ notification: Notification) {
guard
let userInfo = notification.userInfo,
let progress = userInfo[CompactBlockProcessorNotificationKey.progress] as? CompactBlockProgress
else {
return
}
self.notify(progress: progress)
}
@objc func processorStartedDownloading(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
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
guard let self = self, self.status != .validating else { return }
self.status = .validating
}
}
@objc func processorStartedScanning(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self, self.status != .scanning(.nullProgress) else { return }
self.status = .scanning(.nullProgress)
}
}
@objc func processorStartedEnhancing(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self, self.status != .enhancing(NullEnhancementProgress()) else { return }
self.status = .enhancing(NullEnhancementProgress())
}
}
@objc func processorStartedFetching(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self, self.status != .fetching else { return }
self.status = .fetching
}
}
@objc func processorStopped(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
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)
self.status = .error(self.mapError(error))
} else {
self.notifyFailure(
CompactBlockProcessorError.generalError(
message: "This is strange. processorFailed Call received no error message"
)
)
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,
toAddress: Recipient,
memo: Memo?
) async throws -> PendingTransactionEntity {
do {
try await initializer.downloadParametersIfNeeded()
} catch {
throw SynchronizerError.parameterMissing(underlyingError: error)
}
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
)
}
public func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo
) async throws -> PendingTransactionEntity {
// let's see if there are funds to shield
let accountIndex = Int(spendingKey.account)
do {
let tBalance = try await self.getTransparentBalance(accountIndex: accountIndex)
// Verify that at least there are funds for the fee. Ideally this logic will be improved by the shielding wallet.
guard tBalance.verified >= self.network.constants.defaultFee(for: self.latestScannedHeight) else {
throw ShieldFundsError.insuficientTransparentFunds
}
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, recipient: .internalAccount(spendingKey.account), memo: try memo.asMemoBytes(), from: accountIndex)
// 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)
} catch {
throw error
}
}
func createToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
recipient: Recipient,
memo: Memo?
) async throws -> PendingTransactionEntity {
do {
let spend = try transactionManager.initSpend(
zatoshi: zatoshi,
recipient: .address(recipient),
memo: memo?.asMemoBytes(),
from: Int(spendingKey.account)
)
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()
}
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))
}
}
}
public func latestHeight() async throws -> BlockHeight {
try await blockProcessor.downloader.latestBlockHeight()
}
public func latestUTXOs(address: String) async throws -> [UnspentTransactionOutputEntity] {
guard initializer.isValidTransparentAddress(address) else {
throw SynchronizerError.generalError(message: "invalid t-address")
}
let stream = initializer.lightWalletService.fetchUTXOs(for: address, height: network.constants.saplingActivationHeight)
do {
var utxos: [UnspentTransactionOutputEntity] = []
for try await transactionEntity in stream {
utxos.append(transactionEntity)
}
try self.utxoRepository.clearAll(address: address)
try self.utxoRepository.store(utxos: utxos)
return utxos
} catch {
throw SynchronizerError.generalError(message: "\(error)")
}
}
public func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) async throws -> RefreshedUTXOs {
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)
}
public func getUnifiedAddress(accountIndex: Int) async -> UnifiedAddress? {
await blockProcessor.getUnifiedAddress(accountIndex: accountIndex)
}
public func getTransparentAddress(accountIndex: Int) async -> TransparentAddress? {
await blockProcessor.getTransparentAddress(accountIndex: accountIndex)
}
/// Returns the last stored transparent balance
public func getTransparentBalance(accountIndex: Int) async throws -> WalletBalance {
try await blockProcessor.getTransparentBalance(accountIndex: accountIndex)
}
public func rewind(_ policy: RewindPolicy) async throws {
self.stop()
var height: BlockHeight?
switch policy {
case .quick:
break
case .birthday:
let birthday = await self.blockProcessor.config.walletBirthday
height = birthday
case .height(let rewindHeight):
height = rewindHeight
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
private func notify(progress: CompactBlockProgress) {
var userInfo: [AnyHashable: Any] = .init()
userInfo[NotificationKeys.progress] = progress
userInfo[NotificationKeys.blockHeight] = progress.progressHeight
self.status = SyncStatus(progress)
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerProgressUpdated, object: self, userInfo: userInfo)
}
private func notifyStatusChange(newValue: SyncStatus, oldValue: SyncStatus) {
NotificationCenter.default.mainThreadPost(
name: .synchronizerStatusWillUpdate,
object: self,
userInfo:
[
NotificationKeys.currentStatus: oldValue,
NotificationKeys.nextStatus: newValue
]
)
}
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
)
]
)
}
case .unprepared:
break
case .downloading:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerDownloading, object: self)
case .validating:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerValidating, object: self)
case .scanning:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerScanning, object: self)
case .enhancing:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerEnhancing, object: self)
case .fetching:
NotificationCenter.default.mainThreadPost(name: Notification.Name.synchronizerFetching, object: self)
case .error(let e):
self.notifyFailure(e)
}
}
// MARK: book keeping
private func updateMinedTransactions() throws {
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()
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)")
}
}
private func notifyMinedTransaction(_ transaction: PendingTransactionEntity) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
NotificationCenter.default.mainThreadPost(
name: Notification.Name.synchronizerMinedTransaction,
object: self,
userInfo: [NotificationKeys.minedTransaction: transaction]
)
}
}
// swiftlint:disable cyclomatic_complexity
private func mapError(_ error: Error) -> Error {
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)
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
case .invalidAccount:
return SynchronizerError.invalidAccount
case .wrongConsensusBranchId:
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
case .networkMismatch:
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
case .saplingActivationMismatch:
return SynchronizerError.lightwalletdValidationFailed(underlyingError: compactBlockProcessorError)
case .unknown: break
}
}
return SynchronizerError.uncategorized(underlyingError: error)
}
private func notifyFailure(_ error: Error) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
NotificationCenter.default.mainThreadPost(
name: Notification.Name.synchronizerFailed,
object: self,
userInfo: [NotificationKeys.error: self.mapError(error)]
)
}
}
}
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]()
}
}
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
}
}
}
private struct NullEnhancementProgress: EnhancementProgress {
var totalTransactions: Int { 0 }
var enhancedTransactions: Int { 0 }
var lastFoundTransaction: ConfirmedTransactionEntity? { nil }
var range: CompactBlockRange { 0 ... 0 }
}