ZcashLightClientKit/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift

432 lines
16 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
import UIKit
public extension Notification.Name {
static let transactionsUpdated = Notification.Name("SDKSyncronizerTransactionUpdates")
static let synchronizerStarted = Notification.Name("SDKSyncronizerStarted")
static let synchronizerProgressUpdated = Notification.Name("SDKSyncronizerProgressUpdated")
static let synchronizerSynced = Notification.Name("SDKSyncronizerSynced")
static let synchronizerStopped = Notification.Name("SDKSyncronizerStopped")
static let synchronizerDisconnected = Notification.Name("SDKSyncronizerDisconnected")
static let synchronizerSyncing = Notification.Name("SDKSyncronizerSyncing")
static let synchronizerMinedTransaction = Notification.Name("synchronizerMinedTransaction")
2019-12-19 05:04:50 -08:00
static let synchronizerFailed = Notification.Name("SDKSynchronizerFailed")
}
/**
Synchronizer implementation for UIKit and iOS 12+
*/
public class SDKSynchronizer: Synchronizer {
public struct NotificationKeys {
public static let progress = "SDKSynchronizer.progress"
public static let blockHeight = "SDKSynchronizer.blockHeight"
public static let minedTransaction = "SDKSynchronizer.minedTransaction"
2019-12-19 05:04:50 -08:00
public static let error = "SDKSynchronizer.error"
}
public private(set) var status: Status {
didSet {
notify(status: status)
}
}
public private(set) var progress: Float = 0.0
public private(set) var blockProcessor: CompactBlockProcessor?
public private(set) var initializer: Initializer
private var transactionManager: OutboundTransactionManager
private var transactionRepository: TransactionRepository
var taskIdentifier: UIBackgroundTaskIdentifier = .invalid
private var isBackgroundAllowed: Bool {
switch UIApplication.shared.backgroundRefreshStatus {
case .available:
return true
default:
return false
}
}
public init(initializer: Initializer) throws {
self.status = .disconnected
self.initializer = initializer
self.transactionManager = try OutboundTransactionManagerBuilder.build(initializer: initializer)
self.transactionRepository = TransactionRepositoryBuilder.build(initializer: initializer)
}
deinit {
NotificationCenter.default.removeObserver(self)
self.blockProcessor?.stop()
self.blockProcessor = nil
self.taskIdentifier = .invalid
}
public func start() throws {
guard let processor = initializer.blockProcessor() else {
throw SynchronizerError.initFailed
}
subscribeToProcessorNotifications(processor)
registerBackgroundActivity()
self.blockProcessor = processor
guard status == .stopped || status == .disconnected || status == .synced else {
assert(true,"warning: synchronizer started when already started") // TODO: remove this assertion some time in the near future
return
}
try processor.start()
}
public func stop() throws {
guard status != .stopped, status != .disconnected else { return }
guard let processor = self.blockProcessor else { return }
processor.stop(cancelTasks: true)
}
// MARK: event subscription
private func subscribeToAppDelegateNotifications() {
// todo: ios 13 platform specific
let center = NotificationCenter.default
center.addObserver(self,
selector: #selector(applicationDidBecomeActive(_:)),
name: UIApplication.didBecomeActiveNotification,
object: nil)
center.addObserver(self,
selector: #selector(applicationWillTerminate(_:)),
name: UIApplication.willTerminateNotification,
object: nil)
center.addObserver(self,
selector: #selector(applicationWillResignActive(_:)),
name: UIApplication.willResignActiveNotification,
object: nil)
center.addObserver(self,
selector: #selector(applicationDidEnterBackground(_:)),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
center.addObserver(self,
selector: #selector(applicationWillEnterForeground(_:)),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
private func registerBackgroundActivity() {
if self.taskIdentifier == .invalid {
self.taskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
self.blockProcessor?.stop(cancelTasks: true)
UIApplication.shared.endBackgroundTask(self.taskIdentifier)
self.taskIdentifier = .invalid
})
}
}
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(processorStopped(_:)),
name: Notification.Name.blockProcessorStopped,
object: processor)
center.addObserver(self, selector: #selector(processorFailed(_:)),
name: Notification.Name.blockProcessorFailed,
object: processor)
center.addObserver(self,
selector: #selector(processorIdle(_:)),
name: Notification.Name.blockProcessorIdle,
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)
}
// MARK: Block Processor notifications
@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 {
print("error processing reorg notification")
return }
print("handling reorg at: \(progress) with rewind height: \(rewindHeight)")
do {
try transactionManager.handleReorg(at: rewindHeight)
} catch {
2019-12-19 05:04:50 -08:00
print("error handling reorg: \(error)")
notifyFailure(error)
}
}
@objc func processorUpdated(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let progress = userInfo[CompactBlockProcessorNotificationKey.progress] as? Float,
let height = userInfo[CompactBlockProcessorNotificationKey.progressHeight] as? BlockHeight else {
return
}
self.progress = progress
self.notify(progress: progress, height: height)
}
@objc func processorStartedDownloading(_ notification: Notification) {
DispatchQueue.main.async { self.status = .syncing }
}
@objc func processorStartedValidating(_ notification: Notification) {
DispatchQueue.main.async { self.status = .syncing }
}
@objc func processorStartedScanning(_ notification: Notification) {
DispatchQueue.main.async { self.status = .syncing }
}
@objc func processorStopped(_ notification: Notification) {
DispatchQueue.main.async { self.status = .stopped }
}
@objc func processorFailed(_ notification: Notification) {
DispatchQueue.main.async { self.status = .disconnected }
}
@objc func processorIdle(_ notification: Notification) {
DispatchQueue.main.async { self.status = .disconnected }
}
2019-12-19 05:04:50 -08:00
@objc func processorFinished(_ notification: Notification) {
DispatchQueue.global().async {
self.refreshPendingTransactions()
DispatchQueue.main.async {
self.status = .synced
}
}
}
@objc func processorTransitionUnknown(_ notification: Notification) {
self.status = .disconnected
}
// MARK: application notifications
@objc func applicationDidBecomeActive(_ notification: Notification) {
registerBackgroundActivity()
do {
try self.start()
} catch {
self.status = .disconnected
}
}
@objc func applicationDidEnterBackground(_ notification: Notification) {
if !self.isBackgroundAllowed {
do {
try self.stop()
} catch {
self.status = .disconnected
}
}
}
@objc func applicationWillEnterForeground(_ notification: Notification) {
let status = self.status
if status == .stopped || status == .disconnected {
do {
try start()
} catch {
self.status = .disconnected
}
}
}
@objc func applicationWillResignActive(_ notification: Notification) {
do {
try stop()
} catch {
print("stop failed with error: \(error)")
}
}
@objc func applicationWillTerminate(_ notification: Notification) {
do {
try stop()
} catch {}
}
// MARK: Synchronizer methods
public func sendToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
do {
let spend = try transactionManager.initSpend(zatoshi: Int(zatoshi), toAddress: toAddress, memo: memo, from: accountIndex)
transactionManager.encode(spendingKey: spendingKey, pendingTransaction: spend) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let tx):
self.transactionManager.submit(pendingTransaction: tx) { (submitResult) in
switch submitResult {
case .success(let submittedTx):
resultBlock(.success(submittedTx))
case .failure(let submissionError):
DispatchQueue.main.async {
resultBlock(.failure(submissionError))
}
}
}
case .failure(let error):
resultBlock(.failure(error))
}
}
} catch {
resultBlock(.failure(error))
}
}
public func getAddress(accountIndex: Int) -> String {
initializer.getAddress(index: accountIndex) ?? ""
}
public func cancelSpend(transaction: PendingTransactionEntity) -> Bool {
transactionManager.cancel(pendingTransaction: transaction)
}
public var pendingTransactions: [PendingTransactionEntity] {
(try? transactionManager.allPendingTransactions()) ?? [PendingTransactionEntity]()
}
public var clearedTransactions: [ConfirmedTransactionEntity] {
(try? transactionRepository.findAll(offset: 0, limit: Int.max)) ?? [ConfirmedTransactionEntity]()
}
public var sentTransactions: [ConfirmedTransactionEntity] {
(try? transactionRepository.findAllSentTransactions(offset: 0, limit: Int.max)) ?? [ConfirmedTransactionEntity]()
}
public var receivedTransactions: [ConfirmedTransactionEntity] {
(try? transactionRepository.findAllReceivedTransactions(offset: 0, limit: Int.max)) ?? [ConfirmedTransactionEntity]()
}
public func paginatedTransactions(of kind: TransactionKind = .all) -> PaginatedTransactionRepository {
PagedTransactionRepositoryBuilder.build(initializer: initializer, kind: .all)
}
// MARK: notify state
private func notify(progress: Float, height: BlockHeight) {
NotificationCenter.default.post(name: Notification.Name.synchronizerProgressUpdated, object: self, userInfo: [
NotificationKeys.progress : progress,
NotificationKeys.blockHeight : height])
}
private func notify(status: Status) {
switch status {
case .disconnected:
NotificationCenter.default.post(name: Notification.Name.synchronizerDisconnected, object: self)
case .stopped:
NotificationCenter.default.post(name: Notification.Name.synchronizerStopped, object: self)
case .synced:
NotificationCenter.default.post(name: Notification.Name.synchronizerSynced, object: self)
case .syncing:
NotificationCenter.default.post(name: Notification.Name.synchronizerSyncing, object: self)
}
}
// 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 tx = try transactionRepository.findBy(rawId: rawId)
guard let minedHeight = tx?.minedHeight else { return }
let minedTx = try transactionManager.applyMinedHeight(pendingTransaction: pendingTx, minedHeight: minedHeight)
notifyMinedTransaction(minedTx)
})
}
private func removeConfirmedTransactions() throws {
let latestHeight = try transactionRepository.lastScannedHeight()
2020-01-14 14:25:14 -08:00
try transactionManager.allPendingTransactions()?.filter( { abs($0.minedHeight - latestHeight) >= ZcashSDK.DEFAULT_REWIND_DISTANCE } ).forEach( { try transactionManager.delete(pendingTransaction: $0) } )
}
private func refreshPendingTransactions() {
do {
try updateMinedTransactions()
try removeConfirmedTransactions()
} catch {
print("error refreshing pending transactions: \(error)")
}
}
private func notifyMinedTransaction(_ tx: PendingTransactionEntity) {
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.synchronizerMinedTransaction, object: self, userInfo: [NotificationKeys.minedTransaction : tx])
}
}
2019-12-19 05:04:50 -08:00
private func notifyFailure(_ error: Error) {
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.synchronizerFailed, object: self, userInfo: [NotificationKeys.error : error])
}
}
}