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 {
2022-06-22 12:45:37 -07:00
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 )
2019-12-03 07:19:44 -08:00
}
class PersistentTransactionManager : OutboundTransactionManager {
2022-05-31 05:27:24 -07:00
2019-12-03 07:19:44 -08:00
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
) {
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
2022-08-15 13:03:03 -07:00
self . queue = DispatchQueue . init ( label : " PersistentTransactionManager.serial.queue " , qos : . userInitiated )
2019-12-03 07:19:44 -08:00
}
2021-09-17 06:49:58 -07:00
func initSpend (
2022-06-22 12:45:37 -07:00
zatoshi : Zatoshi ,
2021-09-17 06:49:58 -07:00
toAddress : String ,
2022-05-31 05:27:24 -07:00
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
)
2019-12-03 07:19:44 -08:00
}
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
}
2021-09-17 06:49:58 -07:00
func encodeShieldingTransaction (
2022-08-20 15:10:22 -07:00
xprv : TransparentAccountPrivKey ,
2022-09-12 04:27:52 -07:00
pendingTransaction : PendingTransactionEntity
) async throws -> PendingTransactionEntity {
do {
let encodedTransaction = try self . encoder . createShieldingTransaction (
2022-09-12 10:00:40 -07:00
tAccountPrivateKey : xprv ,
memoBytes : try pendingTransaction . memo . intoMemoBytes ( ) ,
2022-09-12 04:27:52 -07:00
from : pendingTransaction . accountIndex
)
let transaction = try self . encoder . expandEncodedTransaction ( encodedTransaction )
2020-12-23 15:01:09 -08:00
2022-09-12 04:27:52 -07: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:00:40 -07:00
result ( . failure ( TransactionManagerError . updateFailed ( pendingTransaction ) ) )
} catch MemoBytes . Errors . invalidUTF8 {
result ( . failure ( TransactionManagerError . shieldingEncodingFailed ( pendingTransaction , reason : " Memo contains invalid UTF-8 bytes " ) ) )
} catch MemoBytes . Errors . tooLong ( let length ) {
result ( . failure ( TransactionManagerError . shieldingEncodingFailed ( pendingTransaction , reason : " Memo is too long. expected 512 bytes, received \( length ) " ) ) )
2022-09-12 04:27:52 -07:00
} catch {
2022-09-12 10:00:40 -07:00
result ( . failure ( error ) )
2020-12-23 15:01:09 -08:00
}
}
2022-09-12 10:00:40 -07:00
2021-09-17 06:49:58 -07:00
func encode (
spendingKey : String ,
2022-09-12 04:27:52 -07:00
pendingTransaction : PendingTransactionEntity
) async throws -> PendingTransactionEntity {
do {
let encodedTransaction = try self . encoder . createTransaction (
spendingKey : spendingKey ,
zatoshi : pendingTransaction . intValue ,
to : pendingTransaction . toAddress ,
memo : pendingTransaction . memo ? . asZcashTransactionMemo ( ) ,
from : pendingTransaction . accountIndex
)
let transaction = try self . encoder . expandEncodedTransaction ( encodedTransaction )
2022-05-31 05:27:24 -07:00
2022-09-12 04:27:52 -07: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 {
throw TransactionManagerError . updateFailed ( pendingTransaction )
} catch {
2019-12-03 07:19:44 -08:00
do {
2022-09-12 04:27:52 -07:00
try self . updateOnFailure ( transaction : pendingTransaction , error : error )
2019-12-03 07:19:44 -08:00
} catch {
2022-09-12 04:27:52 -07:00
throw TransactionManagerError . updateFailed ( pendingTransaction )
2019-12-03 07:19:44 -08:00
}
2022-09-12 04:27:52 -07:00
throw error
2019-12-03 07:19:44 -08:00
}
}
2021-09-17 06:49:58 -07:00
func submit (
2022-09-12 04:27:52 -07:00
pendingTransaction : PendingTransactionEntity
) async throws -> PendingTransactionEntity {
2019-12-03 07:19:44 -08:00
guard let txId = pendingTransaction . id else {
2022-09-12 04:27:52 -07:00
throw TransactionManagerError . notPending ( 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
2019-12-03 07:19:44 -08:00
}
2020-02-26 08:54:48 -08:00
2022-09-12 04:27:52 -07:00
do {
guard let storedTx = try self . repository . find ( by : txId ) else {
throw TransactionManagerError . notPending ( pendingTransaction )
}
2019-12-16 14:25:45 -08:00
2022-09-12 04:27:52 -07:00
guard ! storedTx . isCancelled else {
LoggerProxy . debug ( " ignoring cancelled transaction \( storedTx ) " )
throw TransactionManagerError . cancelled ( storedTx )
2019-12-03 07:19:44 -08:00
}
2022-09-12 04:27:52 -07:00
guard let raw = storedTx . raw else {
LoggerProxy . debug ( " INCONSISTENCY: attempt to send pending transaction \( txId ) that has not raw data " )
throw TransactionManagerError . internalInconsistency ( storedTx )
2019-12-03 07:19:44 -08:00
}
2022-09-12 04:27:52 -07:00
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
2019-12-03 07:19:44 -08:00
}
}
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 )
2019-12-03 07:19:44 -08:00
}
2021-09-17 06:49:58 -07:00
guard var transaction = try repository . find ( by : id ) else {
throw TransactionManagerError . notPending ( pendingTransaction )
2019-12-03 07:19:44 -08:00
}
2021-09-17 06:49:58 -07:00
transaction . minedHeight = minedHeight
2020-07-22 12:32:07 -07:00
guard let pendingTxId = pendingTransaction . id else {
2021-09-17 06:49:58 -07:00
throw TransactionManagerError . updateFailed ( pendingTransaction )
2020-07-22 12:32:07 -07:00
}
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 {
2021-09-17 06:49:58 -07:00
throw TransactionManagerError . updateFailed ( transaction )
2019-12-03 07:19:44 -08:00
}
2021-09-17 06:49:58 -07:00
return transaction
2019-12-03 07:19:44 -08:00
}
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-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 ) }
2019-12-17 09:12:07 -08:00
}
2019-12-03 07:19:44 -08:00
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 }
2019-12-03 07:19:44 -08:00
2021-09-17 06:49:58 -07:00
guard ! transaction . isSubmitted else { return false }
2019-12-03 07:19:44 -08:00
2021-09-17 06:49:58 -07:00
guard ( try ? repository . cancel ( transaction ) ) != nil else { return false }
2019-12-03 07:19:44 -08:00
return true
}
func allPendingTransactions ( ) throws -> [ PendingTransactionEntity ] ? {
try repository . getAll ( )
}
// MARK: o t h e r f u n c t i o n s
2021-09-17 06:49:58 -07:00
private func updateOnFailure ( transaction : PendingTransactionEntity , error : Error ) throws {
var pending = transaction
2019-12-03 07:19:44 -08:00
pending . errorMessage = error . localizedDescription
2021-09-17 06:49:58 -07:00
pending . encodeAttempts = transaction . encodeAttempts + 1
2019-12-03 07:19:44 -08:00
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 {
2021-09-17 06:49:58 -07:00
throw TransactionManagerError . notPending ( pendingTransaction )
2019-12-17 13:16:26 -08:00
}
}
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
}
}