2019-11-14 06:38:54 -08:00
|
|
|
//
|
|
|
|
// SDKSynchronizer.swift
|
|
|
|
// ZcashLightClientKit
|
|
|
|
//
|
|
|
|
// Created by Francisco Gindre on 11/6/19.
|
|
|
|
// Copyright © 2019 Electric Coin Company. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import UIKit
|
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
public extension Notification.Name {
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
|
|
|
Notification is posted whenever transactions are updated
|
|
|
|
|
|
|
|
- Important: not yet posted
|
|
|
|
*/
|
|
|
|
|
|
|
|
static let transactionsUpdated = Notification.Name("SDKSyncronizerTransactionUpdated")
|
|
|
|
|
|
|
|
/**
|
|
|
|
Posted when the synchronizer is started.
|
|
|
|
*/
|
2019-12-16 14:25:45 -08:00
|
|
|
static let synchronizerStarted = Notification.Name("SDKSyncronizerStarted")
|
2020-02-26 08:54:48 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2019-12-16 14:25:45 -08:00
|
|
|
static let synchronizerProgressUpdated = Notification.Name("SDKSyncronizerProgressUpdated")
|
2020-02-26 08:54:48 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
Posted when the synchronizer is synced to latest height
|
|
|
|
*/
|
2019-12-16 14:25:45 -08:00
|
|
|
static let synchronizerSynced = Notification.Name("SDKSyncronizerSynced")
|
2020-02-26 08:54:48 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
Posted when the synchronizer is stopped
|
|
|
|
*/
|
2019-12-16 14:25:45 -08:00
|
|
|
static let synchronizerStopped = Notification.Name("SDKSyncronizerStopped")
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
|
|
|
Posted when the synchronizer loses connection
|
|
|
|
*/
|
2019-12-16 14:25:45 -08:00
|
|
|
static let synchronizerDisconnected = Notification.Name("SDKSyncronizerDisconnected")
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
|
|
|
Posted when the synchronizer starts syncing
|
|
|
|
*/
|
2019-12-16 14:25:45 -08:00
|
|
|
static let synchronizerSyncing = Notification.Name("SDKSyncronizerSyncing")
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
2020-10-20 07:06:31 -07:00
|
|
|
Posted when the synchronizer finds a pendingTransaction that hast been newly mined
|
2020-02-26 08:54:48 -08:00
|
|
|
- Note: query userInfo on NotificationKeys.minedTransaction for the transaction
|
|
|
|
*/
|
2019-12-17 12:11:21 -08:00
|
|
|
static let synchronizerMinedTransaction = Notification.Name("synchronizerMinedTransaction")
|
2020-10-20 07:06:31 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
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")
|
|
|
|
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
|
|
|
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")
|
2019-12-16 14:25:45 -08:00
|
|
|
}
|
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
/**
|
2020-03-26 07:27:55 -07:00
|
|
|
Synchronizer implementation for UIKit and iOS 12+
|
2019-11-14 06:38:54 -08:00
|
|
|
*/
|
|
|
|
public class SDKSynchronizer: Synchronizer {
|
2020-12-23 15:01:09 -08:00
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
public struct NotificationKeys {
|
2019-12-17 12:11:21 -08:00
|
|
|
public static let progress = "SDKSynchronizer.progress"
|
|
|
|
public static let blockHeight = "SDKSynchronizer.blockHeight"
|
|
|
|
public static let minedTransaction = "SDKSynchronizer.minedTransaction"
|
2020-10-20 07:06:31 -07:00
|
|
|
public static let foundTransactions = "SDKSynchronizer.foundTransactions"
|
2019-12-19 05:04:50 -08:00
|
|
|
public static let error = "SDKSynchronizer.error"
|
2019-12-16 14:25:45 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
public private(set) var status: Status {
|
|
|
|
didSet {
|
|
|
|
notify(status: status)
|
|
|
|
}
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
public private(set) var progress: Float = 0.0
|
|
|
|
public private(set) var blockProcessor: CompactBlockProcessor?
|
|
|
|
public private(set) var initializer: Initializer
|
|
|
|
|
2019-12-06 04:38:47 -08:00
|
|
|
private var transactionManager: OutboundTransactionManager
|
|
|
|
private var transactionRepository: TransactionRepository
|
2020-12-11 12:15:29 -08:00
|
|
|
private var utxoRepository: UnspentTransactionOutputRepository
|
2019-11-14 06:38:54 -08:00
|
|
|
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
|
|
|
Creates an SDKSynchronizer instance
|
|
|
|
- Parameter initializer: a wallet Initializer object
|
|
|
|
*/
|
2020-06-03 16:18:57 -07:00
|
|
|
public convenience init(initializer: Initializer) throws {
|
|
|
|
|
|
|
|
self.init(status: .disconnected,
|
|
|
|
initializer: initializer,
|
|
|
|
transactionManager: try OutboundTransactionManagerBuilder.build(initializer: initializer),
|
2020-12-11 12:15:29 -08:00
|
|
|
transactionRepository: initializer.transactionRepository,
|
|
|
|
utxoRepository: try UTXORepositoryBuilder.build(initializer: initializer))
|
2020-06-03 16:18:57 -07:00
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
2020-06-03 16:18:57 -07:00
|
|
|
init(status: Status,
|
|
|
|
initializer: Initializer,
|
|
|
|
transactionManager: OutboundTransactionManager,
|
2020-12-11 12:15:29 -08:00
|
|
|
transactionRepository: TransactionRepository,
|
|
|
|
utxoRepository: UnspentTransactionOutputRepository) {
|
2020-06-03 16:18:57 -07:00
|
|
|
self.status = status
|
|
|
|
self.initializer = initializer
|
|
|
|
self.transactionManager = transactionManager
|
|
|
|
self.transactionRepository = transactionRepository
|
2020-12-11 12:15:29 -08:00
|
|
|
self.utxoRepository = utxoRepository
|
2020-06-03 16:18:57 -07:00
|
|
|
}
|
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
deinit {
|
|
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
|
self.blockProcessor?.stop()
|
|
|
|
self.blockProcessor = nil
|
2020-12-05 13:28:10 -08:00
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
2020-11-24 11:33:58 -08:00
|
|
|
|
|
|
|
private func lazyInitialize() throws {
|
|
|
|
guard self.blockProcessor == nil else { return }
|
|
|
|
guard let processor = initializer.blockProcessor() else {
|
|
|
|
throw SynchronizerError.generalError(message: "compact block processor initialization failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
subscribeToProcessorNotifications(processor)
|
|
|
|
|
|
|
|
self.blockProcessor = processor
|
|
|
|
}
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
|
|
|
Starts the synchronizer
|
|
|
|
- Throws: CompactBlockProcessorError when failures occur
|
|
|
|
*/
|
2020-03-13 17:00:01 -07:00
|
|
|
public func start(retry: Bool = false) throws {
|
2020-04-09 15:25:43 -07:00
|
|
|
|
2020-11-24 11:33:58 -08:00
|
|
|
try lazyInitialize()
|
|
|
|
|
|
|
|
guard let processor = self.blockProcessor else {
|
2020-03-13 17:00:01 -07:00
|
|
|
throw SynchronizerError.generalError(message: "compact block processor initialization failed")
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
guard status == .stopped || status == .disconnected || status == .synced else {
|
2020-03-26 07:27:55 -07:00
|
|
|
assert(true,"warning: synchronizer started when already started") // TODO: remove this assertion sometime in the near future
|
2019-11-14 06:38:54 -08:00
|
|
|
return
|
|
|
|
}
|
2020-01-29 21:34:03 -08:00
|
|
|
|
2020-08-10 15:19:59 -07:00
|
|
|
do {
|
|
|
|
try processor.start(retry: retry)
|
|
|
|
} catch {
|
|
|
|
throw mapError(error)
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
2020-02-26 08:54:48 -08:00
|
|
|
/**
|
|
|
|
Stops the synchronizer
|
|
|
|
*/
|
2020-08-10 15:19:59 -07:00
|
|
|
public func stop() {
|
2020-12-05 13:28:10 -08:00
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
guard status != .stopped, status != .disconnected else { return }
|
|
|
|
|
|
|
|
guard let processor = self.blockProcessor else { return }
|
|
|
|
|
|
|
|
processor.stop(cancelTasks: true)
|
|
|
|
}
|
2020-02-26 08:54:48 -08:00
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
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)
|
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
center.addObserver(self,
|
|
|
|
selector: #selector(processorFinished(_:)),
|
|
|
|
name: Notification.Name.blockProcessorFinished,
|
|
|
|
object: processor)
|
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
center.addObserver(self,
|
|
|
|
selector: #selector(processorTransitionUnknown(_:)),
|
|
|
|
name: Notification.Name.blockProcessorUnknownTransition,
|
|
|
|
object: processor)
|
2019-12-17 09:12:07 -08:00
|
|
|
|
|
|
|
center.addObserver(self,
|
|
|
|
selector: #selector(reorgDetected(_:)),
|
|
|
|
name: Notification.Name.blockProcessorHandledReOrg,
|
|
|
|
object: processor)
|
|
|
|
|
2020-10-20 07:06:31 -07:00
|
|
|
center.addObserver(self,
|
|
|
|
selector: #selector(transactionsFound(_:)),
|
|
|
|
name: Notification.Name.blockProcessorFoundTransactions,
|
|
|
|
object: processor)
|
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
// MARK: Block Processor notifications
|
|
|
|
|
2020-10-20 07:06:31 -07:00
|
|
|
@objc func transactionsFound(_ notification: Notification) {
|
|
|
|
guard let userInfo = notification.userInfo,
|
|
|
|
let foundTransactions = userInfo[CompactBlockProcessorNotificationKey.foundTransactions] as? [ConfirmedTransactionEntity] else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
NotificationCenter.default.post(name: .synchronizerFoundTransactions, object: self, userInfo: [ NotificationKeys.foundTransactions : foundTransactions])
|
|
|
|
}
|
|
|
|
|
2019-12-17 09:12:07 -08:00
|
|
|
@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 {
|
2020-03-09 13:25:27 -07:00
|
|
|
LoggerProxy.debug("error processing reorg notification")
|
2019-12-17 09:12:07 -08:00
|
|
|
return }
|
|
|
|
|
2020-03-09 13:25:27 -07:00
|
|
|
LoggerProxy.debug("handling reorg at: \(progress) with rewind height: \(rewindHeight)")
|
2019-12-17 09:12:07 -08:00
|
|
|
do {
|
2020-01-29 21:34:03 -08:00
|
|
|
try transactionManager.handleReorg(at: rewindHeight)
|
2019-12-17 09:12:07 -08:00
|
|
|
} catch {
|
2020-03-09 13:25:27 -07:00
|
|
|
LoggerProxy.debug("error handling reorg: \(error)")
|
2019-12-19 05:04:50 -08:00
|
|
|
notifyFailure(error)
|
2019-12-17 09:12:07 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-14 06:38:54 -08:00
|
|
|
@objc func processorUpdated(_ notification: Notification) {
|
2019-12-16 14:25:45 -08:00
|
|
|
guard let userInfo = notification.userInfo,
|
2020-01-29 21:34:03 -08:00
|
|
|
let progress = userInfo[CompactBlockProcessorNotificationKey.progress] as? Float,
|
|
|
|
let height = userInfo[CompactBlockProcessorNotificationKey.progressHeight] as? BlockHeight else {
|
|
|
|
return
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
self.progress = progress
|
2019-12-16 14:25:45 -08:00
|
|
|
self.notify(progress: progress, height: height)
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func processorStartedDownloading(_ notification: Notification) {
|
2020-02-26 08:54:48 -08:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.status = .syncing
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func processorStartedValidating(_ notification: Notification) {
|
2020-02-26 08:54:48 -08:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.status = .syncing
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func processorStartedScanning(_ notification: Notification) {
|
2020-02-26 08:54:48 -08:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.status = .syncing
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func processorStopped(_ notification: Notification) {
|
2020-02-26 08:54:48 -08:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.status = .stopped
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func processorFailed(_ notification: Notification) {
|
2020-03-11 19:17:32 -07:00
|
|
|
|
2020-02-26 08:54:48 -08:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
2020-03-11 19:17:32 -07:00
|
|
|
if let error = notification.userInfo?[CompactBlockProcessorNotificationKey.error] as? Error {
|
|
|
|
self.notifyFailure(error)
|
2020-08-10 15:19:59 -07:00
|
|
|
} else {
|
|
|
|
self.notifyFailure(CompactBlockProcessorError.generalError(message: "This is strange. processorFailed Call received no error message"))
|
2020-03-11 19:17:32 -07:00
|
|
|
}
|
2020-02-26 08:54:48 -08:00
|
|
|
self.status = .disconnected
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func processorIdle(_ notification: Notification) {
|
2020-02-26 08:54:48 -08:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.status = .disconnected
|
|
|
|
}
|
2019-12-16 14:25:45 -08:00
|
|
|
}
|
2020-01-29 21:34:03 -08:00
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
@objc func processorFinished(_ notification: Notification) {
|
2020-07-22 12:32:07 -07:00
|
|
|
// FIX: Pending transaction updates fail if done from another thread. Improvement needed: explicitly define queues for sql repositories
|
|
|
|
// DispatchQueue.global().async {[ weak self ] in
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.refreshPendingTransactions()
|
2019-12-17 13:16:26 -08:00
|
|
|
self.status = .synced
|
|
|
|
}
|
2020-07-22 12:32:07 -07:00
|
|
|
// }
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func processorTransitionUnknown(_ notification: Notification) {
|
|
|
|
self.status = .disconnected
|
|
|
|
}
|
|
|
|
|
2019-12-06 04:38:47 -08:00
|
|
|
// MARK: Synchronizer methods
|
|
|
|
|
|
|
|
public func sendToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
|
|
|
|
2020-10-08 10:00:27 -07:00
|
|
|
initializer.downloadParametersIfNeeded { (downloadResult) in
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
switch downloadResult {
|
|
|
|
case .success:
|
|
|
|
self?.createToAddress(spendingKey: spendingKey, zatoshi: zatoshi, toAddress: toAddress, memo: memo, from: accountIndex, resultBlock: resultBlock)
|
|
|
|
case .failure(let error):
|
|
|
|
resultBlock(.failure(SynchronizerError.parameterMissing(underlyingError: error)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-23 15:01:09 -08:00
|
|
|
public func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
|
|
|
|
|
|
|
// let's see if there are funds to shield
|
|
|
|
let derivationTool = DerivationTool.default
|
|
|
|
|
|
|
|
do {
|
|
|
|
let tAddr = try derivationTool.deriveTransparentAddressFromPrivateKey(transparentSecretKey)
|
2021-01-22 13:51:48 -08:00
|
|
|
let tBalance = try utxoRepository.balance(address: tAddr, latestHeight: self.latestDownloadedHeight())
|
2020-12-23 15:01:09 -08:00
|
|
|
|
2021-02-15 11:15:50 -08:00
|
|
|
guard tBalance.confirmed >= ZcashSDK.shieldingThreshold else {
|
2020-12-23 15:01:09 -08:00
|
|
|
resultBlock(.failure(ShieldFundsError.insuficientTransparentFunds))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let vk = try derivationTool.deriveViewingKey(spendingKey: spendingKey)
|
|
|
|
let zAddr = try derivationTool.deriveShieldedAddress(viewingKey: vk)
|
|
|
|
|
2021-01-22 13:51:48 -08:00
|
|
|
let shieldingSpend = try transactionManager.initSpend(zatoshi: Int(tBalance.confirmed), toAddress: zAddr, memo: memo, from: 0)
|
2020-12-23 15:01:09 -08:00
|
|
|
|
|
|
|
transactionManager.encodeShieldingTransaction(spendingKey: spendingKey, tsk: transparentSecretKey, pendingTransaction: shieldingSpend) {[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))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-10-08 10:00:27 -07:00
|
|
|
func createToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
|
|
|
|
2019-12-06 04:38:47 -08:00
|
|
|
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))
|
2019-12-16 14:25:45 -08:00
|
|
|
case .failure(let submissionError):
|
2019-12-06 04:38:47 -08:00
|
|
|
DispatchQueue.main.async {
|
2019-12-16 14:25:45 -08:00
|
|
|
resultBlock(.failure(submissionError))
|
2019-12-06 04:38:47 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-01-29 21:34:03 -08:00
|
|
|
public func allReceivedTransactions() throws -> [ConfirmedTransactionEntity] {
|
|
|
|
try transactionRepository.findAllReceivedTransactions(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
|
2019-12-06 04:38:47 -08:00
|
|
|
}
|
|
|
|
|
2020-01-29 21:34:03 -08:00
|
|
|
public func allPendingTransactions() throws -> [PendingTransactionEntity] {
|
|
|
|
try transactionManager.allPendingTransactions() ?? [PendingTransactionEntity]()
|
2019-12-06 04:38:47 -08:00
|
|
|
}
|
|
|
|
|
2020-01-29 21:34:03 -08:00
|
|
|
public func allClearedTransactions() throws -> [ConfirmedTransactionEntity] {
|
|
|
|
try transactionRepository.findAll(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
|
2019-12-06 04:38:47 -08:00
|
|
|
}
|
|
|
|
|
2020-01-29 21:34:03 -08:00
|
|
|
public func allSentTransactions() throws -> [ConfirmedTransactionEntity] {
|
2020-02-26 08:54:48 -08:00
|
|
|
try transactionRepository.findAllSentTransactions(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
|
2019-12-16 14:25:45 -08:00
|
|
|
}
|
|
|
|
|
2020-10-19 17:01:46 -07:00
|
|
|
public func allConfirmedTransactions(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? {
|
|
|
|
try transactionRepository.findAll(from: transaction, limit: limit)
|
|
|
|
}
|
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
public func paginatedTransactions(of kind: TransactionKind = .all) -> PaginatedTransactionRepository {
|
|
|
|
PagedTransactionRepositoryBuilder.build(initializer: initializer, kind: .all)
|
|
|
|
}
|
|
|
|
|
2020-10-08 10:00:27 -07:00
|
|
|
public func latestDownloadedHeight() throws -> BlockHeight {
|
2020-10-06 16:35:17 -07:00
|
|
|
try initializer.downloader.lastDownloadedBlockHeight()
|
|
|
|
}
|
|
|
|
|
|
|
|
public func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) {
|
|
|
|
initializer.downloader.latestBlockHeight(result: result)
|
|
|
|
}
|
|
|
|
|
|
|
|
public func latestHeight() throws -> BlockHeight {
|
|
|
|
try initializer.downloader.latestBlockHeight()
|
|
|
|
}
|
|
|
|
|
2020-12-11 12:15:29 -08:00
|
|
|
public func latestUTXOs(address: String, result: @escaping (Result<[UnspentTransactionOutputEntity], Error>) -> Void) {
|
|
|
|
guard initializer.isValidTransparentAddress(address) else {
|
|
|
|
result(.failure(SynchronizerError.generalError(message: "invalid t-address")))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
initializer.lightWalletService.fetchUTXOs(for: address, result: { [weak self] r in
|
|
|
|
guard let self = self else { return }
|
|
|
|
switch r {
|
|
|
|
case .success(let utxos):
|
|
|
|
do {
|
|
|
|
try self.utxoRepository.clearAll(address: address)
|
|
|
|
try self.utxoRepository.store(utxos: utxos)
|
|
|
|
result(.success(utxos))
|
|
|
|
} catch {
|
|
|
|
result(.failure(SynchronizerError.generalError(message: "\(error)")))
|
|
|
|
}
|
|
|
|
case .failure(let error):
|
|
|
|
result(.failure(SynchronizerError.connectionFailed(message: error)))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
public func cachedUTXOs(address: String) throws -> [UnspentTransactionOutputEntity] {
|
|
|
|
try utxoRepository.getAll(address: address)
|
|
|
|
}
|
|
|
|
|
2021-01-22 13:51:48 -08:00
|
|
|
/**
|
|
|
|
gets the unshielded balance for the given address.
|
|
|
|
*/
|
|
|
|
public func latestUnshieldedBalance(address: String, result: @escaping (Result<UnshieldedBalance,Error>) -> Void) {
|
|
|
|
latestUTXOs(address: address, result: { [weak self] (r) in
|
|
|
|
|
|
|
|
guard let self = self else { return }
|
|
|
|
switch r {
|
|
|
|
case .success:
|
|
|
|
do {
|
|
|
|
result(.success(try self.utxoRepository.balance(address: address, latestHeight: try self.latestDownloadedHeight())))
|
|
|
|
} catch {
|
|
|
|
result(.failure(SynchronizerError.uncategorized(underlyingError: error)))
|
|
|
|
}
|
|
|
|
case .failure(let e):
|
|
|
|
result(.failure(SynchronizerError.generalError(message: "\(e)")))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
gets the last stored unshielded balance
|
|
|
|
*/
|
|
|
|
public func getUnshieldedBalance(address: String) throws -> UnshieldedBalance {
|
|
|
|
do {
|
|
|
|
let latestHeight = try self.latestDownloadedHeight()
|
2021-02-15 11:15:50 -08:00
|
|
|
let cachedBalance = try utxoRepository.balance(address: address, latestHeight: latestHeight)
|
|
|
|
let pendingShieldingTxValue = try self.allPendingTransactions().filter({ (p) -> Bool in
|
|
|
|
p.isPending(currentHeight: try self.latestDownloadedHeight()) && p.toAddress == address
|
|
|
|
}).reduce(0, { (r, p) -> Int in
|
|
|
|
r + p.value
|
|
|
|
})
|
|
|
|
|
|
|
|
return TransparentBalance(confirmed: max(0,cachedBalance.confirmed - Int64(pendingShieldingTxValue)), unconfirmed: cachedBalance.unconfirmed, address: address)
|
2021-01-22 13:51:48 -08:00
|
|
|
} catch {
|
|
|
|
throw SynchronizerError.uncategorized(underlyingError: error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
// 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)
|
2020-01-29 21:34:03 -08:00
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: book keeping
|
|
|
|
|
2019-12-17 13:16:26 -08:00
|
|
|
private func updateMinedTransactions() throws {
|
2020-01-29 21:34:03 -08:00
|
|
|
try transactionManager.allPendingTransactions()?.filter( { $0.isSubmitSuccess && !$0.isMined } ).forEach( { pendingTx in
|
2019-12-17 13:16:26 -08:00
|
|
|
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-07-22 12:32:07 -07:00
|
|
|
try transactionManager.allPendingTransactions()?.filter( {
|
|
|
|
$0.minedHeight > 0 && abs($0.minedHeight - latestHeight) >= ZcashSDK.DEFAULT_STALE_TOLERANCE }
|
|
|
|
).forEach( {
|
|
|
|
try transactionManager.delete(pendingTransaction: $0)
|
|
|
|
} )
|
2019-12-17 13:16:26 -08:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:25:45 -08:00
|
|
|
private func refreshPendingTransactions() {
|
|
|
|
do {
|
2019-12-17 13:16:26 -08:00
|
|
|
try updateMinedTransactions()
|
|
|
|
try removeConfirmedTransactions()
|
2019-12-16 14:25:45 -08:00
|
|
|
} catch {
|
2020-03-09 13:25:27 -07:00
|
|
|
LoggerProxy.debug("error refreshing pending transactions: \(error)")
|
2019-12-16 14:25:45 -08:00
|
|
|
}
|
2019-12-06 04:38:47 -08:00
|
|
|
}
|
2019-12-17 12:11:21 -08:00
|
|
|
|
|
|
|
private func notifyMinedTransaction(_ tx: PendingTransactionEntity) {
|
|
|
|
DispatchQueue.main.async {
|
2020-02-26 08:54:48 -08:00
|
|
|
[weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
2019-12-17 12:11:21 -08:00
|
|
|
NotificationCenter.default.post(name: Notification.Name.synchronizerMinedTransaction, object: self, userInfo: [NotificationKeys.minedTransaction : tx])
|
|
|
|
}
|
|
|
|
}
|
2019-12-19 05:04:50 -08:00
|
|
|
|
2020-03-13 17:00:01 -07:00
|
|
|
private func mapError(_ error: Error) -> Error {
|
2020-04-06 08:54:31 -07:00
|
|
|
if let compactBlockProcessorError = error as? CompactBlockProcessorError {
|
2020-03-13 17:00:01 -07:00
|
|
|
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)
|
2020-04-09 15:25:43 -07:00
|
|
|
case .grpcError(let statusCode, let message):
|
|
|
|
return SynchronizerError.connectionError(status: statusCode, message: message)
|
2020-08-10 15:19:59 -07:00
|
|
|
case .connectionTimeout:
|
|
|
|
return SynchronizerError.networkTimeout
|
|
|
|
case .unspecifiedError(let underlyingError):
|
|
|
|
return SynchronizerError.uncategorized(underlyingError: underlyingError)
|
|
|
|
case .criticalError:
|
|
|
|
return SynchronizerError.criticalError
|
2020-03-13 17:00:01 -07:00
|
|
|
}
|
|
|
|
}
|
2020-08-10 15:19:59 -07:00
|
|
|
return SynchronizerError.uncategorized(underlyingError: error)
|
2020-03-13 17:00:01 -07:00
|
|
|
}
|
|
|
|
|
2019-12-19 05:04:50 -08:00
|
|
|
private func notifyFailure(_ error: Error) {
|
2020-03-13 17:00:01 -07:00
|
|
|
|
2019-12-19 05:04:50 -08:00
|
|
|
DispatchQueue.main.async {
|
2020-02-26 08:54:48 -08:00
|
|
|
[weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
2020-03-13 17:00:01 -07:00
|
|
|
NotificationCenter.default.post(name: Notification.Name.synchronizerFailed, object: self, userInfo: [NotificationKeys.error : self.mapError(error)])
|
2019-12-19 05:04:50 -08:00
|
|
|
}
|
|
|
|
}
|
2019-11-14 06:38:54 -08:00
|
|
|
}
|
2020-01-29 21:34:03 -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]()
|
|
|
|
}
|
|
|
|
}
|