ZcashLightClientKit/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManage...

274 lines
10 KiB
Swift
Raw Normal View History

//
// PendingTransactionsManager.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/26/19.
//
import Foundation
enum TransactionManagerError: Error {
case couldNotCreateSpend(toAddress: String, account: Int, zatoshi: Zatoshi)
2021-09-17 06:49:58 -07:00
case encodingFailed(PendingTransactionEntity)
case updateFailed(PendingTransactionEntity)
case notPending(PendingTransactionEntity)
case cancelled(PendingTransactionEntity)
case internalInconsistency(PendingTransactionEntity)
case submitFailed(PendingTransactionEntity, errorCode: Int)
case shieldingEncodingFailed(PendingTransactionEntity, reason: String)
}
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
2021-09-17 06:49:58 -07:00
init(
encoder: TransactionEncoder,
service: LightWalletService,
repository: PendingTransactionRepository,
networkType: NetworkType
) {
self.repository = repository
self.encoder = encoder
self.service = service
2021-07-26 16:22:30 -07:00
self.network = networkType
self.queue = DispatchQueue.init(label: "PersistentTransactionManager.serial.queue", qos: .userInitiated)
}
2021-09-17 06:49:58 -07:00
func initSpend(
zatoshi: Zatoshi,
2021-09-17 06:49:58 -07:00
toAddress: String,
memo: MemoBytes,
2021-09-17 06:49:58 -07:00
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
}
2021-09-17 06:49:58 -07:00
func encodeShieldingTransaction(
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
xprv: TransparentAccountPrivKey,
pendingTransaction: PendingTransactionEntity
) async throws -> PendingTransactionEntity {
do {
let encodedTransaction = try self.encoder.createShieldingTransaction(
tAccountPrivateKey: xprv,
memoBytes: try pendingTransaction.memo.intoMemoBytes(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
2020-12-23 15:01:09 -08:00
var pending = pendingTransaction
pending.encodeAttempts += 1
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)
return pending
} catch StorageError.updateFailed {
2022-09-12 10:36:30 -07:00
throw TransactionManagerError.updateFailed(pendingTransaction)
} catch MemoBytes.Errors.invalidUTF8 {
2022-09-12 10:36:30 -07:00
throw TransactionManagerError.shieldingEncodingFailed(pendingTransaction, reason: "Memo contains invalid UTF-8 bytes")
} catch MemoBytes.Errors.tooLong(let length) {
2022-09-12 10:36:30 -07:00
throw TransactionManagerError.shieldingEncodingFailed(pendingTransaction, reason: "Memo is too long. expected 512 bytes, received \(length)")
} catch {
2022-09-12 10:36:30 -07:00
throw error
2020-12-23 15:01:09 -08:00
}
}
2021-09-17 06:49:58 -07:00
func encode(
2022-09-12 10:36:30 -07:00
spendingKey: SaplingExtendedSpendingKey,
pendingTransaction: PendingTransactionEntity
) async throws -> PendingTransactionEntity {
do {
let encodedTransaction = try self.encoder.createTransaction(
spendingKey: spendingKey,
2022-09-12 10:36:30 -07:00
zatoshi: pendingTransaction.value,
to: pendingTransaction.toAddress,
2022-09-12 10:36:30 -07:00
memoBytes: try pendingTransaction.memo.intoMemoBytes(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
var pending = pendingTransaction
pending.encodeAttempts += 1
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)
return pending
} catch StorageError.updateFailed {
throw TransactionManagerError.updateFailed(pendingTransaction)
} catch {
do {
try self.updateOnFailure(transaction: pendingTransaction, error: error)
} catch {
throw TransactionManagerError.updateFailed(pendingTransaction)
}
throw error
}
}
2021-09-17 06:49:58 -07:00
func submit(
pendingTransaction: PendingTransactionEntity
) async throws -> PendingTransactionEntity {
guard let txId = pendingTransaction.id else {
throw TransactionManagerError.notPending(pendingTransaction) // this transaction is not stored
}
do {
guard let storedTx = try self.repository.find(by: txId) else {
throw TransactionManagerError.notPending(pendingTransaction)
}
guard !storedTx.isCancelled else {
LoggerProxy.debug("ignoring cancelled transaction \(storedTx)")
throw TransactionManagerError.cancelled(storedTx)
}
guard let raw = storedTx.raw else {
LoggerProxy.debug("INCONSISTENCY: attempt to send pending transaction \(txId) that has not raw data")
throw TransactionManagerError.internalInconsistency(storedTx)
}
let response = try self.service.submit(spendTransaction: raw)
let transaction = try self.update(transaction: storedTx, on: response)
guard response.errorCode >= 0 else {
throw TransactionManagerError.submitFailed(transaction, errorCode: Int(response.errorCode))
}
return transaction
} catch {
try? self.updateOnFailure(transaction: pendingTransaction, error: error)
throw error
}
}
func applyMinedHeight(pendingTransaction: PendingTransactionEntity, minedHeight: BlockHeight) throws -> PendingTransactionEntity {
guard let id = pendingTransaction.id else {
2021-09-17 06:49:58 -07:00
throw TransactionManagerError.internalInconsistency(pendingTransaction)
}
2021-09-17 06:49:58 -07:00
guard var transaction = try repository.find(by: id) else {
throw TransactionManagerError.notPending(pendingTransaction)
}
2021-09-17 06:49:58 -07:00
transaction.minedHeight = minedHeight
guard let pendingTxId = pendingTransaction.id else {
2021-09-17 06:49:58 -07:00
throw TransactionManagerError.updateFailed(pendingTransaction)
}
do {
try repository.applyMinedHeight(minedHeight, id: pendingTxId)
} catch {
2021-09-17 06:49:58 -07:00
throw TransactionManagerError.updateFailed(transaction)
}
2021-09-17 06:49:58 -07:00
return transaction
}
func handleReorg(at height: BlockHeight) throws {
guard let affectedTxs = try self.allPendingTransactions()?.filter({ $0.minedHeight >= height }) else {
return
}
2021-09-17 06:49:58 -07:00
try affectedTxs
.map { transaction -> PendingTransactionEntity in
var updatedTx = transaction
updatedTx.minedHeight = -1
return updatedTx
}
.forEach { try self.repository.update($0) }
}
func cancel(pendingTransaction: PendingTransactionEntity) -> Bool {
guard let id = pendingTransaction.id else { return false }
2021-09-17 06:49:58 -07:00
guard let transaction = try? repository.find(by: id) else { return false }
2021-09-17 06:49:58 -07:00
guard !transaction.isSubmitted else { return false }
2021-09-17 06:49:58 -07:00
guard (try? repository.cancel(transaction)) != nil else { return false }
return true
}
func allPendingTransactions() throws -> [PendingTransactionEntity]? {
try repository.getAll()
}
// MARK: other functions
2021-09-17 06:49:58 -07:00
private func updateOnFailure(transaction: PendingTransactionEntity, error: Error) throws {
var pending = transaction
pending.errorMessage = error.localizedDescription
2021-09-17 06:49:58 -07:00
pending.encodeAttempts = transaction.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
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
}
func delete(pendingTransaction: PendingTransactionEntity) throws {
do {
try repository.delete(pendingTransaction)
} catch {
2021-09-17 06:49:58 -07:00
throw TransactionManagerError.notPending(pendingTransaction)
}
}
}
2021-09-15 05:21:29 -07:00
enum OutboundTransactionManagerBuilder {
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
)
}
}
2021-09-15 05:21:29 -07:00
enum PendingTransactionRepositoryBuilder {
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 {
static func build(initializer: Initializer) -> TransactionEncoder {
WalletTransactionEncoder(initializer: initializer)
}
}