2019-12-03 07:19:44 -08:00
|
|
|
//
|
|
|
|
// 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)
|
2020-12-23 15:01:09 -08:00
|
|
|
case shieldingEncodingFailed(tx: PendingTransactionEntity, reason: String)
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
class PersistentTransactionManager: OutboundTransactionManager {
|
|
|
|
|
|
|
|
var repository: PendingTransactionRepository
|
|
|
|
var encoder: TransactionEncoder
|
|
|
|
var service: LightWalletService
|
|
|
|
var queue: DispatchQueue
|
2021-07-26 16:22:30 -07:00
|
|
|
var network: NetworkType
|
|
|
|
|
|
|
|
init(encoder: TransactionEncoder,
|
|
|
|
service: LightWalletService,
|
|
|
|
repository: PendingTransactionRepository,
|
|
|
|
networkType: NetworkType) {
|
2019-12-03 07:19:44 -08:00
|
|
|
self.repository = repository
|
|
|
|
self.encoder = encoder
|
|
|
|
self.service = service
|
2021-07-26 16:22:30 -07:00
|
|
|
self.network = networkType
|
2019-12-06 04:38:47 -08:00
|
|
|
self.queue = DispatchQueue.init(label: "PersistentTransactionManager.serial.queue", qos: .userInitiated)
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2020-03-09 13:25:27 -07:00
|
|
|
LoggerProxy.debug("pending transaction \(String(describing: insertedTx.id)) created")
|
2019-12-03 07:19:44 -08:00
|
|
|
return insertedTx
|
|
|
|
}
|
|
|
|
|
2020-12-23 15:01:09 -08:00
|
|
|
func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
|
|
|
queue.async { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
2021-07-26 16:22:30 -07:00
|
|
|
let derivationTool = DerivationTool(networkType: self.network)
|
2021-09-15 05:21:29 -07:00
|
|
|
guard let viewingKey = try? derivationTool.deriveViewingKey(spendingKey: spendingKey),
|
|
|
|
let zAddr = try? derivationTool.deriveShieldedAddress(viewingKey: viewingKey) else {
|
|
|
|
result(
|
|
|
|
.failure(
|
|
|
|
TransactionManagerError.shieldingEncodingFailed(
|
|
|
|
tx: pendingTransaction,
|
|
|
|
reason: "There was an error Deriving your keys")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
2020-12-23 15:01:09 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
guard pendingTransaction.toAddress == zAddr else {
|
2021-09-15 05:21:29 -07:00
|
|
|
result(
|
|
|
|
.failure(
|
|
|
|
TransactionManagerError.shieldingEncodingFailed(
|
|
|
|
tx: pendingTransaction,
|
|
|
|
reason: """
|
|
|
|
the recipient address does not match your
|
|
|
|
derived shielded address. Shielding transactions
|
|
|
|
addresses must match the ones derived from your keys.
|
|
|
|
This is a serious error. We are not letting you encode
|
|
|
|
this shielding transaction because it can lead to loss
|
|
|
|
of funds
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2020-12-23 15:01:09 -08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
do {
|
2021-09-15 05:21:29 -07:00
|
|
|
let encodedTransaction = try self.encoder.createShieldingTransaction(
|
|
|
|
spendingKey: spendingKey,
|
|
|
|
tSecretKey: tsk,
|
|
|
|
memo: pendingTransaction.memo?.asZcashTransactionMemo(),
|
|
|
|
from: pendingTransaction.accountIndex)
|
2020-12-23 15:01:09 -08:00
|
|
|
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
|
|
|
|
|
|
|
|
var pending = pendingTransaction
|
2021-09-15 05:21:29 -07:00
|
|
|
pending.encodeAttempts += 1
|
2020-12-23 15:01:09 -08:00
|
|
|
pending.raw = encodedTransaction.raw
|
|
|
|
pending.rawTransactionId = encodedTransaction.transactionId
|
|
|
|
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
|
|
|
|
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
func encode(spendingKey: String,
|
|
|
|
pendingTransaction: PendingTransactionEntity,
|
|
|
|
result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
2019-12-06 04:38:47 -08:00
|
|
|
queue.async { [weak self] in
|
2019-12-03 07:19:44 -08:00
|
|
|
guard let self = self else { return }
|
|
|
|
do {
|
2021-09-15 05:21:29 -07:00
|
|
|
let encodedTransaction = try self.encoder.createTransaction(
|
|
|
|
spendingKey: spendingKey,
|
|
|
|
zatoshi: pendingTransaction.value,
|
|
|
|
to: pendingTransaction.toAddress,
|
|
|
|
memo: pendingTransaction.memo?.asZcashTransactionMemo(),
|
|
|
|
from: pendingTransaction.accountIndex)
|
|
|
|
|
2020-06-04 14:36:25 -07:00
|
|
|
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
var pending = pendingTransaction
|
2021-09-15 05:21:29 -07:00
|
|
|
pending.encodeAttempts += 1
|
2019-12-03 07:19:44 -08:00
|
|
|
pending.raw = encodedTransaction.raw
|
2019-12-16 14:25:45 -08:00
|
|
|
pending.rawTransactionId = encodedTransaction.transactionId
|
2020-06-04 14:36:25 -07:00
|
|
|
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
|
|
|
|
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
|
2019-12-03 07:19:44 -08:00
|
|
|
try self.repository.update(pending)
|
2019-12-06 04:38:47 -08:00
|
|
|
result(.success(pending))
|
2019-12-03 07:19:44 -08:00
|
|
|
} catch StorageError.updateFailed {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
|
|
|
|
}
|
|
|
|
} catch {
|
2020-12-23 15:01:09 -08:00
|
|
|
do {
|
|
|
|
try self.updateOnFailure(tx: pendingTransaction, error: error)
|
|
|
|
} catch {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
|
|
|
|
}
|
|
|
|
}
|
2019-12-03 07:19:44 -08:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
result(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
func submit(pendingTransaction: PendingTransactionEntity,
|
|
|
|
result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
2019-12-03 07:19:44 -08:00
|
|
|
guard let txId = pendingTransaction.id else {
|
|
|
|
result(.failure(TransactionManagerError.notPending(tx: pendingTransaction)))// this transaction is not stored
|
|
|
|
return
|
|
|
|
}
|
2020-02-26 08:54:48 -08:00
|
|
|
|
2019-12-06 04:38:47 -08:00
|
|
|
queue.async { [weak self] in
|
2019-12-03 07:19:44 -08:00
|
|
|
guard let self = self else { return }
|
2019-12-16 14:25:45 -08:00
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
do {
|
|
|
|
guard let storedTx = try self.repository.find(by: txId) else {
|
|
|
|
result(.failure(TransactionManagerError.notPending(tx: pendingTransaction)))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard !storedTx.isCancelled else {
|
2020-03-09 13:25:27 -07:00
|
|
|
LoggerProxy.debug("ignoring cancelled transaction \(storedTx)")
|
2019-12-03 07:19:44 -08:00
|
|
|
result(.failure(TransactionManagerError.cancelled(tx: storedTx)))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let raw = storedTx.raw else {
|
2020-03-09 13:25:27 -07:00
|
|
|
LoggerProxy.debug("INCONSISTENCY: attempt to send pending transaction \(txId) that has not raw data")
|
2019-12-03 07:19:44 -08:00
|
|
|
result(.failure(TransactionManagerError.internalInconsistency(tx: storedTx)))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let response = try self.service.submit(spendTransaction: raw)
|
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
let transaction = try self.update(transaction: storedTx, on: response)
|
2019-12-03 07:19:44 -08:00
|
|
|
guard response.errorCode >= 0 else {
|
2021-09-15 05:21:29 -07:00
|
|
|
result(.failure(TransactionManagerError.submitFailed(tx: transaction, errorCode: Int(response.errorCode))))
|
2019-12-03 07:19:44 -08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
result(.success(transaction))
|
2019-12-03 07:19:44 -08:00
|
|
|
} catch {
|
2019-12-16 14:25:45 -08:00
|
|
|
try? self.updateOnFailure(tx: pendingTransaction, error: error)
|
|
|
|
result(.failure(error))
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2020-07-22 12:32:07 -07:00
|
|
|
guard let pendingTxId = pendingTransaction.id else {
|
|
|
|
throw TransactionManagerError.updateFailed(tx: pendingTransaction)
|
|
|
|
}
|
2019-12-03 07:19:44 -08:00
|
|
|
do {
|
2020-07-22 12:32:07 -07:00
|
|
|
try repository.applyMinedHeight(minedHeight, id: pendingTxId)
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
} catch {
|
|
|
|
throw TransactionManagerError.updateFailed(tx: tx)
|
|
|
|
}
|
|
|
|
return tx
|
|
|
|
}
|
|
|
|
|
2019-12-17 09:12:07 -08:00
|
|
|
func handleReorg(at height: BlockHeight) throws {
|
|
|
|
guard let affectedTxs = try self.allPendingTransactions()?.filter({ $0.minedHeight >= height }) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
try affectedTxs.map { (transaction) -> PendingTransactionEntity in
|
|
|
|
var updatedTx = transaction
|
2019-12-17 09:12:07 -08:00
|
|
|
updatedTx.minedHeight = -1
|
|
|
|
return updatedTx
|
|
|
|
} .forEach({ try self.repository.update($0) })
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
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 {
|
2021-09-15 05:21:29 -07:00
|
|
|
var pendingTx = transaction
|
|
|
|
pendingTx.submitAttempts += 1
|
2019-12-03 07:19:44 -08:00
|
|
|
let error = sendResponse.errorCode < 0
|
2021-09-15 05:21:29 -07:00
|
|
|
pendingTx.errorCode = error ? Int(sendResponse.errorCode) : nil
|
|
|
|
pendingTx.errorMessage = error ? sendResponse.errorMessage : nil
|
|
|
|
try repository.update(pendingTx)
|
|
|
|
return pendingTx
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
2019-12-17 13:16:26 -08:00
|
|
|
|
|
|
|
func delete(pendingTransaction: PendingTransactionEntity) throws {
|
|
|
|
do {
|
|
|
|
try repository.delete(pendingTransaction)
|
|
|
|
} catch {
|
|
|
|
throw TransactionManagerError.notPending(tx: pendingTransaction)
|
|
|
|
}
|
|
|
|
}
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
2019-12-06 04:38:47 -08:00
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
enum OutboundTransactionManagerBuilder {
|
2019-12-06 04:38:47 -08:00
|
|
|
static func build(initializer: Initializer) throws -> OutboundTransactionManager {
|
2021-09-15 05:21:29 -07:00
|
|
|
PersistentTransactionManager(
|
|
|
|
encoder: TransactionEncoderbuilder.build(initializer: initializer),
|
|
|
|
service: initializer.lightWalletService,
|
|
|
|
repository: try PendingTransactionRepositoryBuilder.build(initializer: initializer),
|
|
|
|
networkType: initializer.network.networkType
|
|
|
|
)
|
2019-12-06 04:38:47 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
enum PendingTransactionRepositoryBuilder {
|
2019-12-06 04:38:47 -08:00
|
|
|
static func build(initializer: Initializer) throws -> PendingTransactionRepository {
|
|
|
|
let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false))
|
|
|
|
try dao.createrTableIfNeeded()
|
|
|
|
return dao
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
enum TransactionEncoderbuilder {
|
2019-12-06 04:38:47 -08:00
|
|
|
static func build(initializer: Initializer) -> TransactionEncoder {
|
2019-12-16 14:25:45 -08:00
|
|
|
WalletTransactionEncoder(initializer: initializer)
|
2019-12-06 04:38:47 -08:00
|
|
|
}
|
|
|
|
}
|