ZcashLightClientKit/ZcashLightClientKit/Transaction/PersistentTransactionManage...

210 lines
8.3 KiB
Swift

//
// PendingTransactionsManager.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/26/19.
//
import Foundation
enum TransactionManagerError: Error {
case couldNotCreateSpend(toAddress: String, account: Int, zatoshi: Int)
case encodingFailed(tx: PendingTransactionEntity)
case updateFailed(tx: PendingTransactionEntity)
case notPending(tx: PendingTransactionEntity)
case cancelled(tx: PendingTransactionEntity)
case internalInconsistency(tx: PendingTransactionEntity)
case submitFailed(tx: PendingTransactionEntity, errorCode: Int)
}
class PersistentTransactionManager: OutboundTransactionManager {
var repository: PendingTransactionRepository
var encoder: TransactionEncoder
var service: LightWalletService
var queue: DispatchQueue
init(encoder: TransactionEncoder, service: LightWalletService, repository: PendingTransactionRepository) {
self.repository = repository
self.encoder = encoder
self.service = service
self.queue = DispatchQueue.init(label: "PersistentTransactionManager.serial.queue", qos: .userInitiated)
}
func initSpend(zatoshi: Int, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity {
guard let insertedTx = try repository.find(by: try repository.create(PendingTransaction(value: zatoshi, toAddress: toAddress, memo: memo, account: accountIndex))) else {
throw TransactionManagerError.couldNotCreateSpend(toAddress: toAddress, account: accountIndex, zatoshi: zatoshi)
}
LoggerProxy.debug("pending transaction \(String(describing: insertedTx.id)) created")
return insertedTx
}
func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
queue.async { [weak self] in
guard let self = self else { return }
do {
let encodedTransaction = try self.encoder.createTransaction(spendingKey: spendingKey, zatoshi: pendingTransaction.value, to: pendingTransaction.toAddress, memo: pendingTransaction.memo?.asZcashTransactionMemo(), from: pendingTransaction.accountIndex)
var pending = pendingTransaction
pending.encodeAttempts = pending.encodeAttempts + 1
pending.raw = encodedTransaction.raw
pending.rawTransactionId = encodedTransaction.transactionId
try self.repository.update(pending)
result(.success(pending))
} catch StorageError.updateFailed {
DispatchQueue.main.async {
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
}
} catch {
DispatchQueue.main.async {
result(.failure(error))
}
}
}
}
func submit(pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
guard let txId = pendingTransaction.id else {
result(.failure(TransactionManagerError.notPending(tx: pendingTransaction)))// this transaction is not stored
return
}
queue.async { [weak self] in
guard let self = self else { return }
do {
guard let storedTx = try self.repository.find(by: txId) else {
result(.failure(TransactionManagerError.notPending(tx: pendingTransaction)))
return
}
guard !storedTx.isCancelled else {
LoggerProxy.debug("ignoring cancelled transaction \(storedTx)")
result(.failure(TransactionManagerError.cancelled(tx: storedTx)))
return
}
guard let raw = storedTx.raw else {
LoggerProxy.debug("INCONSISTENCY: attempt to send pending transaction \(txId) that has not raw data")
result(.failure(TransactionManagerError.internalInconsistency(tx: storedTx)))
return
}
let response = try self.service.submit(spendTransaction: raw)
let tx = try self.update(transaction: storedTx, on: response)
guard response.errorCode >= 0 else {
result(.failure(TransactionManagerError.submitFailed(tx: tx, errorCode: Int(response.errorCode))))
return
}
result(.success(tx))
} catch {
try? self.updateOnFailure(tx: pendingTransaction, error: error)
result(.failure(error))
}
}
}
func applyMinedHeight(pendingTransaction: PendingTransactionEntity, minedHeight: BlockHeight) throws -> PendingTransactionEntity {
guard let id = pendingTransaction.id else {
throw TransactionManagerError.internalInconsistency(tx: pendingTransaction)
}
guard var tx = try repository.find(by: id) else {
throw TransactionManagerError.notPending(tx: pendingTransaction)
}
tx.minedHeight = minedHeight
do {
try repository.update(tx)
} catch {
throw TransactionManagerError.updateFailed(tx: tx)
}
return tx
}
func handleReorg(at height: BlockHeight) throws {
guard let affectedTxs = try self.allPendingTransactions()?.filter({ $0.minedHeight >= height }) else {
return
}
try affectedTxs.map { (tx) -> PendingTransactionEntity in
var updatedTx = tx
updatedTx.minedHeight = -1
return updatedTx
} .forEach({ try self.repository.update($0) })
}
func monitorChanges(byId: Int, observer: Any) {
// TODO: Implement this
}
func cancel(pendingTransaction: PendingTransactionEntity) -> Bool {
guard let id = pendingTransaction.id else { return false }
guard let tx = try? repository.find(by: id) else { return false }
guard !tx.isSubmitted else { return false }
guard (try? repository.cancel(tx)) != nil else { return false }
return true
}
func allPendingTransactions() throws -> [PendingTransactionEntity]? {
try repository.getAll()
}
// MARK: other functions
private func updateOnFailure(tx: PendingTransactionEntity, error: Error) throws {
var pending = tx
pending.errorMessage = error.localizedDescription
pending.encodeAttempts = tx.encodeAttempts + 1
try self.repository.update(pending)
}
private func update(transaction: PendingTransactionEntity, on sendResponse: LightWalletServiceResponse) throws -> PendingTransactionEntity {
var tx = transaction
tx.submitAttempts = tx.submitAttempts + 1
let error = sendResponse.errorCode < 0
tx.errorCode = Int(sendResponse.errorCode)
tx.errorMessage = error ? sendResponse.errorMessage : nil
try repository.update(tx)
return tx
}
func delete(pendingTransaction: PendingTransactionEntity) throws {
do {
try repository.delete(pendingTransaction)
} catch {
throw TransactionManagerError.notPending(tx: pendingTransaction)
}
}
}
class OutboundTransactionManagerBuilder {
static func build(initializer: Initializer) throws -> OutboundTransactionManager {
return PersistentTransactionManager(encoder: TransactionEncoderbuilder.build(initializer: initializer), service: initializer.lightWalletService, repository: try PendingTransactionRepositoryBuilder.build(initializer: initializer))
}
}
class PendingTransactionRepositoryBuilder {
static func build(initializer: Initializer) throws -> PendingTransactionRepository {
let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false))
try dao.createrTableIfNeeded()
return dao
}
}
class TransactionEncoderbuilder {
static func build(initializer: Initializer) -> TransactionEncoder {
WalletTransactionEncoder(initializer: initializer)
}
}