2019-12-03 07:19:44 -08:00
//
// P e n d i n g T r a n s a c t i o n s M a n a g e r . s w i f t
// Z c a s h L i g h t C l i e n t K i t
//
// C r e a t e d b y F r a n c i s c o G i n d r e o n 1 1 / 2 6 / 1 9 .
//
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 )
2020-12-23 15:01:09 -08:00
guard let vk = try ? derivationTool . deriveViewingKey ( spendingKey : spendingKey ) ,
let zAddr = try ? derivationTool . deriveShieldedAddress ( viewingKey : vk ) else {
result ( . failure ( TransactionManagerError . shieldingEncodingFailed ( tx : pendingTransaction , reason : " There was an error Deriving your keys " ) ) )
return
}
guard pendingTransaction . toAddress = = zAddr else {
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 " ) ) )
return
}
do {
let encodedTransaction = try self . encoder . createShieldingTransaction ( spendingKey : spendingKey , tSecretKey : tsk , memo : pendingTransaction . memo ? . asZcashTransactionMemo ( ) , from : pendingTransaction . accountIndex )
let transaction = try self . encoder . expandEncodedTransaction ( encodedTransaction )
var pending = pendingTransaction
pending . encodeAttempts = 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 )
result ( . success ( pending ) )
} catch StorageError . updateFailed {
DispatchQueue . main . async {
result ( . failure ( TransactionManagerError . updateFailed ( tx : pendingTransaction ) ) )
}
} catch {
DispatchQueue . main . async {
result ( . failure ( error ) )
}
}
}
}
2019-12-03 07:19:44 -08:00
func encode ( spendingKey : String , pendingTransaction : PendingTransactionEntity , result : @ escaping ( Result < PendingTransactionEntity , Error > ) -> Void ) {
2019-12-16 14:25:45 -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 }
do {
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
2019-12-16 14:25:45 -08:00
pending . encodeAttempts = 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 ) )
}
}
}
}
func submit ( pendingTransaction : PendingTransactionEntity , result : @ escaping ( Result < PendingTransactionEntity , Error > ) -> Void ) {
guard let txId = pendingTransaction . id else {
result ( . failure ( TransactionManagerError . notPending ( tx : pendingTransaction ) ) ) // t h i s t r a n s a c t i o n i s n o t s t o r e d
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 )
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 {
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
}
try affectedTxs . map { ( tx ) -> PendingTransactionEntity in
var updatedTx = tx
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: I m p l e m e n t t h i s
}
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: o t h e r f u n c t i o n s
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
2019-12-16 14:25:45 -08:00
tx . submitAttempts = tx . submitAttempts + 1
2019-12-03 07:19:44 -08:00
let error = sendResponse . errorCode < 0
2020-06-03 16:18:57 -07:00
tx . errorCode = error ? Int ( sendResponse . errorCode ) : nil
2019-12-03 07:19:44 -08:00
tx . errorMessage = error ? sendResponse . errorMessage : nil
try repository . update ( tx )
return tx
}
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
class OutboundTransactionManagerBuilder {
static func build ( initializer : Initializer ) throws -> OutboundTransactionManager {
2021-07-28 09:59:10 -07:00
return 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
}
}
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 {
2019-12-16 14:25:45 -08:00
WalletTransactionEncoder ( initializer : initializer )
2019-12-06 04:38:47 -08:00
}
}