Transaction submission + tests (#34)
* transaction manager * Transaction Manager Tests * pending transactions DAO + Scaffold tests * PendingTransactionsDao + tests * Persistent Transaction Manager
This commit is contained in:
parent
c772934d3d
commit
d8affaebd3
|
@ -22,4 +22,6 @@ enum StorageError: Error {
|
||||||
case openFailed
|
case openFailed
|
||||||
case closeFailed
|
case closeFailed
|
||||||
case operationFailed
|
case operationFailed
|
||||||
|
case updateFailed
|
||||||
|
case malformedEntity(fields: [String]?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,37 +6,179 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SQLite
|
||||||
|
struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case toAddress = "to_address"
|
||||||
|
case accountIndex = "account_index"
|
||||||
|
case minedHeight = "mined_height"
|
||||||
|
case expiryHeight = "expiry_height"
|
||||||
|
case cancelled
|
||||||
|
case encodeAttempts = "encode_attempts"
|
||||||
|
case submitAttempts = "submit_attempts"
|
||||||
|
case errorMessage = "error_message"
|
||||||
|
case errorCode = "error_code"
|
||||||
|
case createTime = "create_time"
|
||||||
|
case raw
|
||||||
|
case id
|
||||||
|
case value
|
||||||
|
case memo
|
||||||
|
case rawTransactionId = "txid"
|
||||||
|
}
|
||||||
|
|
||||||
|
var toAddress: String
|
||||||
|
var accountIndex: Int
|
||||||
|
var minedHeight: BlockHeight
|
||||||
|
var expiryHeight: BlockHeight
|
||||||
|
var cancelled: Int
|
||||||
|
var encodeAttempts: Int
|
||||||
|
var submitAttempts: Int
|
||||||
|
var errorMessage: String?
|
||||||
|
var errorCode: Int?
|
||||||
|
var createTime: TimeInterval
|
||||||
|
var raw: Data?
|
||||||
|
var id: Int?
|
||||||
|
var value: Int
|
||||||
|
var memo: Data?
|
||||||
|
var rawTransactionId: Data?
|
||||||
|
|
||||||
|
func isSameTransactionId<T>(other: T) -> Bool where T : RawIdentifiable {
|
||||||
|
self.rawTransactionId == other.rawTransactionId
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from(entity: PendingTransactionEntity) -> PendingTransaction {
|
||||||
|
PendingTransaction(toAddress: entity.toAddress,
|
||||||
|
accountIndex: entity.accountIndex,
|
||||||
|
minedHeight: entity.minedHeight,
|
||||||
|
expiryHeight: entity.expiryHeight,
|
||||||
|
cancelled: entity.cancelled,
|
||||||
|
encodeAttempts: entity.encodeAttempts,
|
||||||
|
submitAttempts: entity.submitAttempts,
|
||||||
|
errorMessage: entity.errorMessage,
|
||||||
|
errorCode: entity.errorCode,
|
||||||
|
createTime: entity.createTime,
|
||||||
|
raw: entity.raw,
|
||||||
|
id: entity.id,
|
||||||
|
value: entity.value,
|
||||||
|
memo: entity.memo,
|
||||||
|
rawTransactionId: entity.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PendingTransaction {
|
||||||
|
|
||||||
|
// TODO: Handle Memo
|
||||||
|
init(value: Int, toAddress: String, memo: String?, account index: Int) {
|
||||||
|
self = PendingTransaction(toAddress: toAddress, accountIndex: index, minedHeight: -1, expiryHeight: -1, cancelled: 0, encodeAttempts: 0, submitAttempts: 0, errorMessage: nil, errorCode: nil, createTime: Date().timeIntervalSince1970, raw: nil, id: nil, value: Int(value), memo: nil, rawTransactionId: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PendingTransactionSQLDAO: PendingTransactionRepository {
|
class PendingTransactionSQLDAO: PendingTransactionRepository {
|
||||||
|
|
||||||
|
let table = Table("pending_transactions")
|
||||||
|
|
||||||
|
struct TableColumns {
|
||||||
|
static var toAddress = Expression<String>("to_address")
|
||||||
|
static var accountIndex = Expression<Int>("account_index")
|
||||||
|
static var minedHeight = Expression<Int?>("mined_height")
|
||||||
|
static var expiryHeight = Expression<Int?>("expiry_height")
|
||||||
|
static var cancelled = Expression<Int?>("cancelled")
|
||||||
|
static var encodeAttempts = Expression<Int?>("encode_attempts")
|
||||||
|
static var errorMessage = Expression<String?>("error_message")
|
||||||
|
static var submitAttempts = Expression<Int?>("submit_attempts")
|
||||||
|
static var errorCode = Expression<Int?>("error_code")
|
||||||
|
static var createTime = Expression<TimeInterval?>("create_time")
|
||||||
|
static var raw = Expression<Blob?>("raw")
|
||||||
|
static var id = Expression<Int>("id")
|
||||||
|
static var value = Expression<Int>("value")
|
||||||
|
static var memo = Expression<Blob?>("memo")
|
||||||
|
static var rawTransactionId = Expression<Blob?>("txid")
|
||||||
|
}
|
||||||
|
|
||||||
var dbProvider: ConnectionProvider
|
var dbProvider: ConnectionProvider
|
||||||
|
|
||||||
init(dbProvider: ConnectionProvider) {
|
init(dbProvider: ConnectionProvider) {
|
||||||
self.dbProvider = dbProvider
|
self.dbProvider = dbProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func create(_ transaction: PendingTransactionEntity) throws -> Int64 {
|
func createrTableIfNeeded() throws {
|
||||||
-1
|
let statement = table.create(ifNotExists: true) { t in
|
||||||
|
t.column(TableColumns.id, primaryKey: .autoincrement)
|
||||||
|
t.column(TableColumns.toAddress)
|
||||||
|
t.column(TableColumns.accountIndex)
|
||||||
|
t.column(TableColumns.minedHeight)
|
||||||
|
t.column(TableColumns.expiryHeight)
|
||||||
|
t.column(TableColumns.cancelled)
|
||||||
|
t.column(TableColumns.encodeAttempts, defaultValue: 0)
|
||||||
|
t.column(TableColumns.errorMessage)
|
||||||
|
t.column(TableColumns.errorCode)
|
||||||
|
t.column(TableColumns.submitAttempts, defaultValue: 0)
|
||||||
|
t.column(TableColumns.createTime)
|
||||||
|
t.column(TableColumns.rawTransactionId)
|
||||||
|
t.column(TableColumns.value)
|
||||||
|
t.column(TableColumns.raw)
|
||||||
|
t.column(TableColumns.memo)
|
||||||
|
}
|
||||||
|
|
||||||
|
try dbProvider.connection().run(statement)
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(_ transaction: PendingTransactionEntity) throws -> Int {
|
||||||
|
|
||||||
|
let tx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
|
||||||
|
|
||||||
|
return try Int(dbProvider.connection().run(table.insert(tx)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(_ transaction: PendingTransactionEntity) throws {
|
func update(_ transaction: PendingTransactionEntity) throws {
|
||||||
|
let tx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
|
||||||
|
guard let id = tx.id else {
|
||||||
|
throw StorageError.malformedEntity(fields: ["id"])
|
||||||
|
}
|
||||||
|
try dbProvider.connection().run(table.filter(TableColumns.id == id).update(tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(_ transaction: PendingTransactionEntity) throws {
|
func delete(_ transaction: PendingTransactionEntity) throws {
|
||||||
|
guard let id = transaction.id else {
|
||||||
|
throw StorageError.malformedEntity(fields: ["id"])
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try dbProvider.connection().run(table.filter(TableColumns.id == id).delete())
|
||||||
|
} catch {
|
||||||
|
throw StorageError.updateFailed
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancel(_ transaction: PendingTransactionEntity) throws {
|
func cancel(_ transaction: PendingTransactionEntity) throws {
|
||||||
|
|
||||||
|
var tx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
|
||||||
|
tx.cancelled = 1
|
||||||
|
guard let id = tx.id else {
|
||||||
|
throw StorageError.malformedEntity(fields: ["id"])
|
||||||
|
}
|
||||||
|
try dbProvider.connection().run(table.filter(TableColumns.id == id).update(tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func find(by id: Int64) throws -> PendingTransactionEntity? {
|
func find(by id: Int) throws -> PendingTransactionEntity? {
|
||||||
nil
|
guard let row = try dbProvider.connection().pluck(table.filter(TableColumns.id == id).limit(1)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let tx: PendingTransaction = try row.decode()
|
||||||
|
return tx
|
||||||
|
} catch {
|
||||||
|
throw StorageError.operationFailed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAll() throws -> [PendingTransactionEntity] {
|
func getAll() throws -> [PendingTransactionEntity] {
|
||||||
[]
|
let allTxs: [PendingTransaction] = try dbProvider.connection().prepare(table).map({ row in
|
||||||
|
try row.decode()
|
||||||
|
})
|
||||||
|
return allTxs
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ struct TransactionBuilder {
|
||||||
blockTimeInSeconds: TimeInterval(integerLiteral: blockTimeInSeconds),
|
blockTimeInSeconds: TimeInterval(integerLiteral: blockTimeInSeconds),
|
||||||
transactionIndex: Int(transactionIndex),
|
transactionIndex: Int(transactionIndex),
|
||||||
raw: raw,
|
raw: raw,
|
||||||
id: UInt(id),
|
id: Int(id),
|
||||||
value: Int(value),
|
value: Int(value),
|
||||||
memo: memo,
|
memo: memo,
|
||||||
rawTransactionId: transactionId)
|
rawTransactionId: transactionId)
|
||||||
|
@ -110,7 +110,7 @@ struct TransactionBuilder {
|
||||||
blockTimeInSeconds: TimeInterval(integerLiteral: blockTimeInSeconds),
|
blockTimeInSeconds: TimeInterval(integerLiteral: blockTimeInSeconds),
|
||||||
transactionIndex: Int(transactionIndex),
|
transactionIndex: Int(transactionIndex),
|
||||||
raw: nil,
|
raw: nil,
|
||||||
id: UInt(id),
|
id: Int(id),
|
||||||
value: Int(value),
|
value: Int(value),
|
||||||
memo: memo,
|
memo: memo,
|
||||||
rawTransactionId: transactionId)
|
rawTransactionId: transactionId)
|
||||||
|
|
|
@ -36,7 +36,7 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
|
||||||
var blockTimeInSeconds: TimeInterval
|
var blockTimeInSeconds: TimeInterval
|
||||||
var transactionIndex: Int
|
var transactionIndex: Int
|
||||||
var raw: Data?
|
var raw: Data?
|
||||||
var id: UInt
|
var id: Int?
|
||||||
var value: Int
|
var value: Int
|
||||||
var memo: Data?
|
var memo: Data?
|
||||||
var rawTransactionId: Data?
|
var rawTransactionId: Data?
|
||||||
|
@ -45,12 +45,12 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
|
||||||
class TransactionSQLDAO: TransactionRepository {
|
class TransactionSQLDAO: TransactionRepository {
|
||||||
|
|
||||||
struct TableStructure {
|
struct TableStructure {
|
||||||
static var id = Expression<Int64>(Transaction.CodingKeys.id.rawValue)
|
static var id = Expression<Int>(Transaction.CodingKeys.id.rawValue)
|
||||||
static var transactionId = Expression<Blob>(Transaction.CodingKeys.transactionId.rawValue)
|
static var transactionId = Expression<Blob>(Transaction.CodingKeys.transactionId.rawValue)
|
||||||
static var created = Expression<String?>(Transaction.CodingKeys.created.rawValue)
|
static var created = Expression<String?>(Transaction.CodingKeys.created.rawValue)
|
||||||
static var txIndex = Expression<String?>(Transaction.CodingKeys.transactionIndex.rawValue)
|
static var txIndex = Expression<String?>(Transaction.CodingKeys.transactionIndex.rawValue)
|
||||||
static var expiryHeight = Expression<Int64?>(Transaction.CodingKeys.expiryHeight.rawValue)
|
static var expiryHeight = Expression<Int?>(Transaction.CodingKeys.expiryHeight.rawValue)
|
||||||
static var minedHeight = Expression<Int64?>(Transaction.CodingKeys.minedHeight.rawValue)
|
static var minedHeight = Expression<Int?>(Transaction.CodingKeys.minedHeight.rawValue)
|
||||||
static var raw = Expression<Blob?>(Transaction.CodingKeys.raw.rawValue)
|
static var raw = Expression<Blob?>(Transaction.CodingKeys.raw.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,8 +70,8 @@ class TransactionSQLDAO: TransactionRepository {
|
||||||
try dbProvider.connection().scalar(transactions.filter(TableStructure.minedHeight == nil).count)
|
try dbProvider.connection().scalar(transactions.filter(TableStructure.minedHeight == nil).count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findBy(id: Int64) throws -> TransactionEntity? {
|
func findBy(id: Int) throws -> TransactionEntity? {
|
||||||
let query = transactions.filter(TableStructure.id == Int64(id)).limit(1)
|
let query = transactions.filter(TableStructure.id == id).limit(1)
|
||||||
let sequence = try dbProvider.connection().prepare(query)
|
let sequence = try dbProvider.connection().prepare(query)
|
||||||
let entity: Transaction? = try sequence.map({ try $0.decode() }).first
|
let entity: Transaction? = try sequence.map({ try $0.decode() }).first
|
||||||
return entity
|
return entity
|
||||||
|
|
|
@ -15,7 +15,7 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
|
||||||
var cancelled: Int { get set }
|
var cancelled: Int { get set }
|
||||||
var encodeAttempts: Int { get set }
|
var encodeAttempts: Int { get set }
|
||||||
var submitAttempts: Int { get set }
|
var submitAttempts: Int { get set }
|
||||||
var errorMesssage: String? { get set }
|
var errorMessage: String? { get set }
|
||||||
var errorCode: Int? { get set }
|
var errorCode: Int? { get set }
|
||||||
var createTime: TimeInterval { get set }
|
var createTime: TimeInterval { get set }
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ public extension PendingTransactionEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
var isFailedSubmit: Bool {
|
var isFailedSubmit: Bool {
|
||||||
errorMesssage != nil || (errorCode != nil && (errorCode ?? 0) < 0)
|
errorMessage != nil || (errorCode != nil && (errorCode ?? 0) < 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isFailure: Bool {
|
var isFailure: Bool {
|
||||||
|
@ -72,6 +72,6 @@ public extension PendingTransactionEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSubmitSuccess: Bool {
|
var isSubmitSuccess: Bool {
|
||||||
submitAttempts > 0 && (errorCode != nil && (errorCode ?? -1) >= 0) && errorMesssage == nil
|
submitAttempts > 0 && (errorCode != nil && (errorCode ?? -1) >= 0) && errorMessage == nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public extension TransactionEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol AbstractTransaction {
|
public protocol AbstractTransaction {
|
||||||
var id: UInt { get set }
|
var id: Int? { get set }
|
||||||
var value: Int { get set }
|
var value: Int { get set }
|
||||||
var memo: Data? { get set }
|
var memo: Data? { get set }
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol PendingTransactionRepository {
|
protocol PendingTransactionRepository {
|
||||||
func create(_ transaction: PendingTransactionEntity) throws -> Int64
|
func create(_ transaction: PendingTransactionEntity) throws -> Int
|
||||||
func update(_ transaction: PendingTransactionEntity) throws
|
func update(_ transaction: PendingTransactionEntity) throws
|
||||||
func delete(_ transaction: PendingTransactionEntity) throws
|
func delete(_ transaction: PendingTransactionEntity) throws
|
||||||
func cancel(_ transaction: PendingTransactionEntity) throws
|
func cancel(_ transaction: PendingTransactionEntity) throws
|
||||||
func find(by id: Int64) throws -> PendingTransactionEntity?
|
func find(by id: Int) throws -> PendingTransactionEntity?
|
||||||
func getAll() throws -> [PendingTransactionEntity]
|
func getAll() throws -> [PendingTransactionEntity]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ enum TransactionRepositoryError: Error {
|
||||||
protocol TransactionRepository {
|
protocol TransactionRepository {
|
||||||
func countAll() throws -> Int
|
func countAll() throws -> Int
|
||||||
func countUnmined() throws -> Int
|
func countUnmined() throws -> Int
|
||||||
func findBy(id: Int64) throws -> TransactionEntity?
|
func findBy(id: Int) throws -> TransactionEntity?
|
||||||
func findBy(rawId: Data) throws -> TransactionEntity?
|
func findBy(rawId: Data) throws -> TransactionEntity?
|
||||||
func findAllSentTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?
|
func findAllSentTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?
|
||||||
func findAllReceivedTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?
|
func findAllReceivedTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?
|
||||||
|
|
|
@ -51,6 +51,24 @@ public class LightWalletGRPCService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LightWalletGRPCService: LightWalletService {
|
extension LightWalletGRPCService: LightWalletService {
|
||||||
|
public func submit(spendTransaction: Data, result: @escaping (Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||||
|
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try self.compactTxStreamer.sendTransaction(RawTransaction(serializedData: spendTransaction))
|
||||||
|
result(.success(response))
|
||||||
|
} catch {
|
||||||
|
result(.failure(LightWalletServiceError.genericError(error: error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
|
||||||
|
try compactTxStreamer.sendTransaction(RawTransaction(serializedData: spendTransaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] {
|
public func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] {
|
||||||
var blocks = [ZcashCompactBlock]()
|
var blocks = [ZcashCompactBlock]()
|
||||||
|
@ -142,3 +160,4 @@ extension LightWalletGRPCService: LightWalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,13 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftGRPC
|
import SwiftGRPC
|
||||||
|
import SwiftProtobuf
|
||||||
public enum LightWalletServiceError: Error {
|
public enum LightWalletServiceError: Error {
|
||||||
case generalError
|
case generalError
|
||||||
case failed(statusCode: StatusCode)
|
case failed(statusCode: StatusCode)
|
||||||
case invalidBlock
|
case invalidBlock
|
||||||
|
case sentFailed(sendResponse: LightWalletServiceResponse)
|
||||||
|
case genericError(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LightWalletServiceError: Equatable {
|
extension LightWalletServiceError: Equatable {
|
||||||
|
@ -40,11 +42,28 @@ extension LightWalletServiceError: Equatable {
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case .sentFailed(let sendResponse):
|
||||||
|
switch rhs {
|
||||||
|
case .sentFailed(let response):
|
||||||
|
return response.errorCode == sendResponse.errorCode
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .genericError(_):
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol LightWalletServiceResponse {
|
||||||
|
var errorCode: Int32 { get }
|
||||||
|
var errorMessage: String { get }
|
||||||
|
var unknownFields: SwiftProtobuf.UnknownStorage { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SendResponse: LightWalletServiceResponse {}
|
||||||
|
|
||||||
public protocol LightWalletService {
|
public protocol LightWalletService {
|
||||||
/**
|
/**
|
||||||
Return the latest block height known to the service.
|
Return the latest block height known to the service.
|
||||||
|
@ -79,4 +98,16 @@ public protocol LightWalletService {
|
||||||
*/
|
*/
|
||||||
func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock]
|
func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock]
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Submits a raw transaction over lightwalletd. Non-Blocking
|
||||||
|
*/
|
||||||
|
func submit(spendTransaction: Data, result: @escaping(Result<LightWalletServiceResponse,LightWalletServiceError>) -> Void)
|
||||||
|
|
||||||
|
/**
|
||||||
|
Submits a raw transaction over lightwalletd. Blocking
|
||||||
|
*/
|
||||||
|
|
||||||
|
func submit(spendTransaction: Data) throws -> LightWalletServiceResponse
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
//
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
print("pending transaction \(String(describing: insertedTx.id)) created")
|
||||||
|
return insertedTx
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
||||||
|
// FIX: change to async when librustzcash is updated to v6
|
||||||
|
queue.sync { [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 = 1
|
||||||
|
pending.raw = encodedTransaction.raw
|
||||||
|
pending.rawTransactionId = encodedTransaction.raw
|
||||||
|
try self.repository.update(pending)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// FIX: change to async when librustzcash is updated to v6
|
||||||
|
queue.sync { [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 {
|
||||||
|
print("ignoring cancelled transaction \(storedTx)")
|
||||||
|
result(.failure(TransactionManagerError.cancelled(tx: storedTx)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let raw = storedTx.raw else {
|
||||||
|
print("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 {
|
||||||
|
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 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
|
||||||
|
|
||||||
|
let error = sendResponse.errorCode < 0
|
||||||
|
tx.errorCode = Int(sendResponse.errorCode)
|
||||||
|
tx.errorMessage = error ? sendResponse.errorMessage : nil
|
||||||
|
try repository.update(tx)
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,8 +10,8 @@ import Foundation
|
||||||
typealias TransactionEncoderResultBlock = (_ result: Result<EncodedTransaction,Error>) -> Void
|
typealias TransactionEncoderResultBlock = (_ result: Result<EncodedTransaction,Error>) -> Void
|
||||||
|
|
||||||
public enum TransactionEncoderError: Error {
|
public enum TransactionEncoderError: Error {
|
||||||
case notFound(transactionId: Int64)
|
case notFound(transactionId: Int)
|
||||||
case NotEncoded(transactionId: Int64)
|
case NotEncoded(transactionId: Int)
|
||||||
case missingParams
|
case missingParams
|
||||||
case spendingKeyWrongNetwork
|
case spendingKeyWrongNetwork
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ protocol TransactionEncoder {
|
||||||
double-bangs for things).
|
double-bangs for things).
|
||||||
Blocking
|
Blocking
|
||||||
*/
|
*/
|
||||||
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction
|
func createTransaction(spendingKey: String, zatoshi: Int, to: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates a transaction, throwing an exception whenever things are missing. When the provided wallet implementation
|
Creates a transaction, throwing an exception whenever things are missing. When the provided wallet implementation
|
||||||
|
@ -32,6 +32,6 @@ protocol TransactionEncoder {
|
||||||
double-bangs for things).
|
double-bangs for things).
|
||||||
Non-blocking
|
Non-blocking
|
||||||
*/
|
*/
|
||||||
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock)
|
func createTransaction(spendingKey: String, zatoshi: Int, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
// TransactionManager.swift
|
||||||
|
// ZcashLightClientKit
|
||||||
|
//
|
||||||
|
// Created by Francisco Gindre on 11/26/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Manage outbound transactions with the main purpose of reporting which ones are still pending,
|
||||||
|
particularly after failed attempts or dropped connectivity. The intent is to help see outbound
|
||||||
|
transactions through to completion.
|
||||||
|
*/
|
||||||
|
|
||||||
|
protocol OutboundTransactionManager {
|
||||||
|
func initSpend(zatoshi: Int, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity
|
||||||
|
|
||||||
|
func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void)
|
||||||
|
|
||||||
|
func submit(pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void)
|
||||||
|
|
||||||
|
func applyMinedHeight(pendingTransaction: PendingTransactionEntity, minedHeight: BlockHeight) throws -> PendingTransactionEntity
|
||||||
|
|
||||||
|
func monitorChanges(byId: Int, observer: Any) // check this out. code smell
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attempts to Cancel a transaction. Returns true if successful
|
||||||
|
*/
|
||||||
|
func cancel(pendingTransaction: PendingTransactionEntity) -> Bool
|
||||||
|
|
||||||
|
func allPendingTransactions() throws -> [PendingTransactionEntity]?
|
||||||
|
}
|
|
@ -8,16 +8,19 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class WalletTransactionEncoder: TransactionEncoder {
|
class WalletTransactionEncoder: TransactionEncoder {
|
||||||
|
|
||||||
var rustBackend: ZcashRustBackend.Type
|
var rustBackend: ZcashRustBackend.Type
|
||||||
var repository: TransactionRepository
|
var repository: TransactionRepository
|
||||||
var initializer: Initializer
|
var initializer: Initializer
|
||||||
|
var queue: DispatchQueue
|
||||||
init(rust: ZcashRustBackend.Type, repository: TransactionRepository, initializer: Initializer) {
|
init(rust: ZcashRustBackend.Type, repository: TransactionRepository, initializer: Initializer) {
|
||||||
self.rustBackend = rust
|
self.rustBackend = rust
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.initializer = initializer
|
self.initializer = initializer
|
||||||
|
self.queue = DispatchQueue(label: "wallet.transaction.encoder.serial.queue")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction {
|
func createTransaction(spendingKey: String, zatoshi: Int, to: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction {
|
||||||
|
|
||||||
let txId = try createSpend(spendingKey: spendingKey, zatoshi: zatoshi, to: to, memo: memo, from: accountIndex)
|
let txId = try createSpend(spendingKey: spendingKey, zatoshi: zatoshi, to: to, memo: memo, from: accountIndex)
|
||||||
|
|
||||||
|
@ -35,9 +38,9 @@ class WalletTransactionEncoder: TransactionEncoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) {
|
func createTransaction(spendingKey: String, zatoshi: Int, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) {
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
queue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
do {
|
do {
|
||||||
result(.success(try self.createTransaction(spendingKey: spendingKey, zatoshi: zatoshi, to: to, memo: memo, from: accountIndex)))
|
result(.success(try self.createTransaction(spendingKey: spendingKey, zatoshi: zatoshi, to: to, memo: memo, from: accountIndex)))
|
||||||
|
@ -47,7 +50,7 @@ class WalletTransactionEncoder: TransactionEncoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSpend(spendingKey: String, zatoshi: Int64, to address: String, memo: String?, from accountIndex: Int) throws -> Int64 {
|
func createSpend(spendingKey: String, zatoshi: Int, to address: String, memo: String?, from accountIndex: Int) throws -> Int {
|
||||||
guard ensureParams(spend: initializer.spendParamsURL, output: initializer.spendParamsURL),
|
guard ensureParams(spend: initializer.spendParamsURL, output: initializer.spendParamsURL),
|
||||||
let spend = URL(string: initializer.spendParamsURL.path), let output = URL(string: initializer.outputParamsURL.path) else {
|
let spend = URL(string: initializer.spendParamsURL.path), let output = URL(string: initializer.outputParamsURL.path) else {
|
||||||
throw TransactionEncoderError.missingParams
|
throw TransactionEncoderError.missingParams
|
||||||
|
@ -60,7 +63,7 @@ class WalletTransactionEncoder: TransactionEncoder {
|
||||||
throw rustBackend.lastError() ?? RustWeldingError.genericError(message: "create spend failed")
|
throw rustBackend.lastError() ?? RustWeldingError.genericError(message: "create spend failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return txId
|
return Int(txId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureParams(spend: URL, output: URL) -> Bool {
|
func ensureParams(spend: URL, output: URL) -> Bool {
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
//
|
||||||
|
// OutboundTransactionManagerTests.swift
|
||||||
|
// ZcashLightClientKit-Unit-Tests
|
||||||
|
//
|
||||||
|
// Created by Francisco Gindre on 11/26/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import ZcashLightClientKit
|
||||||
|
class OutboundTransactionManagerTests: XCTestCase {
|
||||||
|
|
||||||
|
var transactionManager: OutboundTransactionManager!
|
||||||
|
var encoder: TransactionEncoder!
|
||||||
|
var pendingRespository: PendingTransactionSQLDAO!
|
||||||
|
var dataDbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedDataDbURL()!)
|
||||||
|
var cacheDbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedCacheDbURL()!)
|
||||||
|
var pendingDbhandle = TestDbHandle(originalDb: try! TestDbBuilder.pendingTransactionsDbURL())
|
||||||
|
var service: LightWalletService!
|
||||||
|
var initializer: Initializer!
|
||||||
|
let spendingKey = "secret-extended-key-test1qvpevftsqqqqpqy52ut2vv24a2qh7nsukew7qg9pq6djfwyc3xt5vaxuenshp2hhspp9qmqvdh0gs2ljpwxders5jkwgyhgln0drjqaguaenfhehz4esdl4kwlm5t9q0l6wmzcrvcf5ed6dqzvct3e2ge7f6qdvzhp02m7sp5a0qjssrwpdh7u6tq89hl3wchuq8ljq8r8rwd6xdwh3nry9at80z7amnj3s6ah4jevnvfr08gxpws523z95g6dmn4wm6l3658kd4xcq9rc0qn"
|
||||||
|
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
|
||||||
|
let zpend: Int = 500_000
|
||||||
|
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
|
||||||
|
try! dataDbHandle.setUp()
|
||||||
|
try! cacheDbHandle.setUp()
|
||||||
|
pendingRespository = PendingTransactionSQLDAO(dbProvider: pendingDbhandle.connectionProvider(readwrite: true))
|
||||||
|
|
||||||
|
try! pendingRespository.createrTableIfNeeded()
|
||||||
|
|
||||||
|
initializer = Initializer(cacheDbURL: cacheDbHandle.readWriteDb, dataDbURL: dataDbHandle.readWriteDb, endpoint: LightWalletEndpointBuilder.default, spendParamsURL: try! __spendParamsURL(), outputParamsURL: try! __outputParamsURL())
|
||||||
|
|
||||||
|
encoder = WalletTransactionEncoder(rust: ZcashRustBackend.self, repository: TestDbBuilder.transactionRepository()!, initializer: initializer)
|
||||||
|
transactionManager = PersistentTransactionManager(encoder: encoder, service: MockLightWalletService(latestBlockHeight: 620999), repository: pendingRespository)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
transactionManager = nil
|
||||||
|
encoder = nil
|
||||||
|
service = nil
|
||||||
|
initializer = nil
|
||||||
|
pendingRespository = nil
|
||||||
|
dataDbHandle.dispose()
|
||||||
|
cacheDbHandle.dispose()
|
||||||
|
pendingDbhandle.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitSpend() {
|
||||||
|
// This is an example of a functional test case.
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
|
|
||||||
|
var tx: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { tx = try transactionManager.initSpend(zatoshi: zpend, toAddress: recipientAddress, memo: nil, from: 0) }())
|
||||||
|
|
||||||
|
guard let pendingTx = tx else {
|
||||||
|
XCTFail("failed to create pending transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(pendingTx.toAddress, recipientAddress)
|
||||||
|
XCTAssertEqual(pendingTx.memo, nil)
|
||||||
|
XCTAssertEqual(pendingTx.value, zpend)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeSpend() {
|
||||||
|
let expect = XCTestExpectation(description: self.description)
|
||||||
|
|
||||||
|
var tx: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { tx = try transactionManager.initSpend(zatoshi: zpend, toAddress: recipientAddress, memo: nil, from: 0) }())
|
||||||
|
|
||||||
|
guard let pendingTx = tx else {
|
||||||
|
XCTFail("failed to create pending transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionManager.encode(spendingKey: spendingKey, pendingTransaction: pendingTx) { (result) in
|
||||||
|
expect.fulfill()
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .failure(let error):
|
||||||
|
XCTFail("failed with error: \(error)")
|
||||||
|
case .success(let tx):
|
||||||
|
XCTAssertEqual(tx.id, pendingTx.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wait(for: [expect], timeout: 120)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func testSubmit() {
|
||||||
|
|
||||||
|
let encodeExpect = XCTestExpectation(description: "encode")
|
||||||
|
|
||||||
|
let submitExpect = XCTestExpectation(description: "submit")
|
||||||
|
var tx: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { tx = try transactionManager.initSpend(zatoshi: zpend, toAddress: recipientAddress, memo: nil, from: 0) }())
|
||||||
|
|
||||||
|
guard let pendingTx = tx else {
|
||||||
|
XCTFail("failed to create pending transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var encodedTx: PendingTransactionEntity?
|
||||||
|
transactionManager.encode(spendingKey: spendingKey, pendingTransaction: pendingTx) { (result) in
|
||||||
|
encodeExpect.fulfill()
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .failure(let error):
|
||||||
|
XCTFail("failed with error: \(error)")
|
||||||
|
case .success(let tx):
|
||||||
|
XCTAssertEqual(tx.id, pendingTx.id)
|
||||||
|
encodedTx = tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wait(for: [encodeExpect], timeout: 500)
|
||||||
|
|
||||||
|
guard let submittableTx = encodedTx else {
|
||||||
|
XCTFail("failed to encode tx")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
transactionManager.submit(pendingTransaction: submittableTx) { (result) in
|
||||||
|
submitExpect.fulfill()
|
||||||
|
switch result {
|
||||||
|
case .failure(let error):
|
||||||
|
XCTFail("submission failed with error: \(error)")
|
||||||
|
case .success(let successfulTx):
|
||||||
|
XCTAssertEqual(submittableTx.id, successfulTx.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
wait(for: [submitExpect], timeout: 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func testApplyMinedHeight() {
|
||||||
|
var tx: PendingTransactionEntity?
|
||||||
|
|
||||||
|
let minedHeight = 789_000
|
||||||
|
XCTAssertNoThrow(try { tx = try transactionManager.initSpend(zatoshi: zpend, toAddress: recipientAddress, memo: nil, from: 0) }())
|
||||||
|
|
||||||
|
guard let pendingTx = tx else {
|
||||||
|
XCTFail("failed to create pending transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var minedTransaction: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { minedTransaction = try transactionManager.applyMinedHeight(pendingTransaction: pendingTx, minedHeight: minedHeight)}())
|
||||||
|
|
||||||
|
guard let minedTx = minedTransaction else {
|
||||||
|
XCTFail("failed to apply mined height")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(minedTx.isMined)
|
||||||
|
XCTAssertEqual(minedTx.minedHeight, minedHeight)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCancel() {
|
||||||
|
|
||||||
|
var tx: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { tx = try transactionManager.initSpend(zatoshi: zpend, toAddress: recipientAddress, memo: nil, from: 0) }())
|
||||||
|
|
||||||
|
guard let pendingTx = tx else {
|
||||||
|
XCTFail("failed to create pending transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancellationResult = transactionManager.cancel(pendingTransaction: pendingTx)
|
||||||
|
|
||||||
|
guard let id = pendingTx.id else {
|
||||||
|
XCTFail("transaction with no id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let retrievedTransaction = try! pendingRespository.find(by: id) else {
|
||||||
|
XCTFail("failed to retrieve previously created transation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(cancellationResult, retrievedTransaction.isCancelled)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func testAllPendingTransactions() {
|
||||||
|
|
||||||
|
let txCount = 100
|
||||||
|
for i in 0 ..< txCount {
|
||||||
|
var tx: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { tx = try transactionManager.initSpend(zatoshi: zpend, toAddress: recipientAddress, memo: nil, from: 0) }())
|
||||||
|
guard tx != nil else {
|
||||||
|
XCTFail("failed to create pending transaction \(i)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let allPending = try! transactionManager.allPendingTransactions() else {
|
||||||
|
XCTFail("failed to retrieve all pending transactions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(allPending.count, txCount)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
//
|
||||||
|
// PendingTransactionRepositoryTests.swift
|
||||||
|
// ZcashLightClientKit-Unit-Tests
|
||||||
|
//
|
||||||
|
// Created by Francisco Gindre on 11/27/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import ZcashLightClientKit
|
||||||
|
class PendingTransactionRepositoryTests: XCTestCase {
|
||||||
|
|
||||||
|
var pendingRepository: PendingTransactionRepository!
|
||||||
|
|
||||||
|
let dbUrl = try! TestDbBuilder.pendingTransactionsDbURL()
|
||||||
|
|
||||||
|
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
|
||||||
|
override func setUp() {
|
||||||
|
cleanUpDb()
|
||||||
|
let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: try! TestDbBuilder.pendingTransactionsDbURL().absoluteString))
|
||||||
|
try! dao.createrTableIfNeeded()
|
||||||
|
pendingRepository = dao
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
cleanUpDb()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpDb() {
|
||||||
|
try? FileManager.default.removeItem(at: TestDbBuilder.pendingTransactionsDbURL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreate() {
|
||||||
|
|
||||||
|
let tx = createAndStoreMockedTransaction()
|
||||||
|
|
||||||
|
guard let id = tx.id, id >= 0 else {
|
||||||
|
XCTFail("failed to create mocked transaction that was just inserted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTx: PendingTransactionEntity?
|
||||||
|
XCTAssertNoThrow(try { expectedTx = try pendingRepository.find(by: id)}())
|
||||||
|
|
||||||
|
guard let expected = expectedTx else {
|
||||||
|
XCTFail("failed to retrieve mocked transaction by id \(id) that was just inserted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(tx.accountIndex, expected.accountIndex)
|
||||||
|
XCTAssertEqual(tx.value, expected.value)
|
||||||
|
XCTAssertEqual(tx.toAddress, expected.toAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFindById() {
|
||||||
|
let tx = createAndStoreMockedTransaction()
|
||||||
|
|
||||||
|
var expected: PendingTransactionEntity?
|
||||||
|
|
||||||
|
guard let id = tx.id else {
|
||||||
|
XCTFail("transaction with no id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertNoThrow(try { expected = try pendingRepository.find(by: id)}())
|
||||||
|
|
||||||
|
XCTAssertNotNil(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCancel() {
|
||||||
|
let tx = createAndStoreMockedTransaction()
|
||||||
|
guard let id = tx.id else {
|
||||||
|
XCTFail("transaction with no id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard id >= 0 else {
|
||||||
|
XCTFail("failed to create mocked transaction that was just inserted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try pendingRepository.cancel(tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDelete() {
|
||||||
|
let tx = createAndStoreMockedTransaction()
|
||||||
|
guard let id = tx.id else {
|
||||||
|
XCTFail("transaction with no id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard id >= 0 else {
|
||||||
|
XCTFail("failed to create mocked transaction that was just inserted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try pendingRepository.delete(tx))
|
||||||
|
|
||||||
|
var unexpectedTx: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { unexpectedTx = try pendingRepository.find(by: id) }())
|
||||||
|
|
||||||
|
XCTAssertNil(unexpectedTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetAll() {
|
||||||
|
var mockTransactions = [PendingTransactionEntity]()
|
||||||
|
for _ in 1...100 {
|
||||||
|
mockTransactions.append(createAndStoreMockedTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
var all: [PendingTransactionEntity]?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { all = try pendingRepository.getAll() }())
|
||||||
|
|
||||||
|
guard let allTxs = all else {
|
||||||
|
XCTFail("failed to get all transactions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(mockTransactions.count, allTxs.count)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdate() {
|
||||||
|
let newAccountIndex = 1
|
||||||
|
let newValue: Int = 123_456
|
||||||
|
let tx = createAndStoreMockedTransaction()
|
||||||
|
guard let id = tx.id else {
|
||||||
|
XCTFail("transaction with no id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var stored: PendingTransactionEntity?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { stored = try pendingRepository.find(by: id)}())
|
||||||
|
|
||||||
|
guard stored != nil else {
|
||||||
|
XCTFail("failed to store tx")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stored!.accountIndex = newAccountIndex
|
||||||
|
stored!.value = newValue
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try pendingRepository.update(stored!))
|
||||||
|
|
||||||
|
guard let updatedTransaction = try? pendingRepository.find(by: stored!.id!) else {
|
||||||
|
XCTFail("failed to retrieve updated transaction with id: \(stored!.id!)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(updatedTransaction.value, newValue)
|
||||||
|
XCTAssertEqual(updatedTransaction.accountIndex, newAccountIndex)
|
||||||
|
XCTAssertEqual(updatedTransaction.toAddress, stored!.toAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAndStoreMockedTransaction() -> PendingTransactionEntity {
|
||||||
|
var tx = mockTransaction()
|
||||||
|
var id: Int?
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try { id = try pendingRepository.create(tx) }())
|
||||||
|
tx.id = Int(id ?? -1)
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformanceExample() {
|
||||||
|
// This is an example of a performance test case.
|
||||||
|
self.measure {
|
||||||
|
_ = try! pendingRepository.getAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mockTransaction() -> PendingTransactionEntity {
|
||||||
|
PendingTransaction(value: Int.random(in: 1 ... 1_000_000), toAddress: recipientAddress, memo: nil, account: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ class WalletTransactionEncoderTests: XCTestCase {
|
||||||
var initializer: Initializer!
|
var initializer: Initializer!
|
||||||
let spendingKey = "secret-extended-key-test1qvpevftsqqqqpqy52ut2vv24a2qh7nsukew7qg9pq6djfwyc3xt5vaxuenshp2hhspp9qmqvdh0gs2ljpwxders5jkwgyhgln0drjqaguaenfhehz4esdl4kwlm5t9q0l6wmzcrvcf5ed6dqzvct3e2ge7f6qdvzhp02m7sp5a0qjssrwpdh7u6tq89hl3wchuq8ljq8r8rwd6xdwh3nry9at80z7amnj3s6ah4jevnvfr08gxpws523z95g6dmn4wm6l3658kd4xcq9rc0qn"
|
let spendingKey = "secret-extended-key-test1qvpevftsqqqqpqy52ut2vv24a2qh7nsukew7qg9pq6djfwyc3xt5vaxuenshp2hhspp9qmqvdh0gs2ljpwxders5jkwgyhgln0drjqaguaenfhehz4esdl4kwlm5t9q0l6wmzcrvcf5ed6dqzvct3e2ge7f6qdvzhp02m7sp5a0qjssrwpdh7u6tq89hl3wchuq8ljq8r8rwd6xdwh3nry9at80z7amnj3s6ah4jevnvfr08gxpws523z95g6dmn4wm6l3658kd4xcq9rc0qn"
|
||||||
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
|
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
|
||||||
let zpend: Int64 = 500_000
|
let zpend: Int = 500_000
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
try! dataDbHandle.setUp()
|
try! dataDbHandle.setUp()
|
||||||
|
@ -54,7 +54,7 @@ class WalletTransactionEncoderTests: XCTestCase {
|
||||||
|
|
||||||
XCTAssert(initializer.getBalance() >= zpend)
|
XCTAssert(initializer.getBalance() >= zpend)
|
||||||
|
|
||||||
var spendId: Int64?
|
var spendId: Int?
|
||||||
|
|
||||||
XCTAssertNoThrow(try { spendId = try transactionEncoder.createSpend(spendingKey: self.spendingKey, zatoshi: self.zpend, to: self.recipientAddress, memo: nil, from: 0) }())
|
XCTAssertNoThrow(try { spendId = try transactionEncoder.createSpend(spendingKey: self.spendingKey, zatoshi: self.zpend, to: self.recipientAddress, memo: nil, from: 0) }())
|
||||||
|
|
||||||
|
|
|
@ -11,16 +11,20 @@ import XCTest
|
||||||
|
|
||||||
class ZcashRustBackendTests: XCTestCase {
|
class ZcashRustBackendTests: XCTestCase {
|
||||||
var dbData: URL!
|
var dbData: URL!
|
||||||
|
var dataDbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedDataDbURL()!)
|
||||||
|
var cacheDbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedCacheDbURL()!)
|
||||||
|
let spendingKey = "secret-extended-key-test1qvpevftsqqqqpqy52ut2vv24a2qh7nsukew7qg9pq6djfwyc3xt5vaxuenshp2hhspp9qmqvdh0gs2ljpwxders5jkwgyhgln0drjqaguaenfhehz4esdl4kwlm5t9q0l6wmzcrvcf5ed6dqzvct3e2ge7f6qdvzhp02m7sp5a0qjssrwpdh7u6tq89hl3wchuq8ljq8r8rwd6xdwh3nry9at80z7amnj3s6ah4jevnvfr08gxpws523z95g6dmn4wm6l3658kd4xcq9rc0qn"
|
||||||
|
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
|
||||||
|
let zpend: Int = 500_000
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
dbData = try! __dataDbURL()
|
dbData = try! __dataDbURL()
|
||||||
|
try? dataDbHandle.setUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
|
|
||||||
try? FileManager.default.removeItem(at: dbData!)
|
try? FileManager.default.removeItem(at: dbData!)
|
||||||
|
dataDbHandle.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInitAndGetAddress() {
|
func testInitAndGetAddress() {
|
||||||
|
@ -60,4 +64,11 @@ class ZcashRustBackendTests: XCTestCase {
|
||||||
XCTAssertTrue(ZcashRustBackend.scanBlocks(dbCache: cacheDb, dbData: dbData))
|
XCTAssertTrue(ZcashRustBackend.scanBlocks(dbCache: cacheDb, dbData: dbData))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSendToAddress() {
|
||||||
|
|
||||||
|
let tx = try! ZcashRustBackend.sendToAddress(dbData: dataDbHandle.readWriteDb, account: 0, extsk: spendingKey, to: recipientAddress, value: Int64(zpend), memo: nil, spendParams: URL(string: __spendParamsURL().path)!, outputParams: URL(string: __outputParamsURL().path)!)
|
||||||
|
XCTAssert(tx > 0)
|
||||||
|
XCTAssertNil(ZcashRustBackend.lastError())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftProtobuf
|
||||||
@testable import ZcashLightClientKit
|
@testable import ZcashLightClientKit
|
||||||
|
|
||||||
|
struct LightWalletServiceMockResponse: LightWalletServiceResponse {
|
||||||
|
var errorCode: Int32
|
||||||
|
var errorMessage: String
|
||||||
|
var unknownFields: UnknownStorage
|
||||||
|
}
|
||||||
|
|
||||||
class MockLightWalletService: LightWalletService {
|
class MockLightWalletService: LightWalletService {
|
||||||
|
|
||||||
private var service = LightWalletGRPCService(channel: ChannelProvider().channel())
|
private var service = LightWalletGRPCService(channel: ChannelProvider().channel())
|
||||||
|
@ -34,5 +42,13 @@ class MockLightWalletService: LightWalletService {
|
||||||
try self.service.blockRange(range)
|
try self.service.blockRange(range)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func submit(spendTransaction: Data, result: @escaping (Result<LightWalletServiceResponse, LightWalletServiceError>) -> Void) {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
|
||||||
|
result(.success(LightWalletServiceMockResponse(errorCode: 0, errorMessage: "", unknownFields: UnknownStorage())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
|
||||||
|
return LightWalletServiceMockResponse(errorCode: 0, errorMessage: "", unknownFields: UnknownStorage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,19 @@ class AwfulLightWalletService: LightWalletService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func submit(spendTransaction: Data, result: @escaping(Result<LightWalletServiceResponse,LightWalletServiceError>) -> Void) {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
result(.failure(LightWalletServiceError.generalError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Submits a raw transaction over lightwalletd. Blocking
|
||||||
|
*/
|
||||||
|
|
||||||
|
func submit(spendTransaction: Data) throws -> LightWalletServiceResponse {
|
||||||
|
throw LightWalletServiceError.generalError
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockRustBackend: ZcashRustBackendWelding {
|
class MockRustBackend: ZcashRustBackendWelding {
|
||||||
|
|
|
@ -50,6 +50,9 @@ class TestDbBuilder {
|
||||||
return compactBlockDao
|
return compactBlockDao
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func pendingTransactionsDbURL() throws -> URL {
|
||||||
|
try __documentsDirectory().appendingPathComponent("pending.db")
|
||||||
|
}
|
||||||
static func prePopulatedCacheDbURL() -> URL? {
|
static func prePopulatedCacheDbURL() -> URL? {
|
||||||
Bundle(for: TestDbBuilder.self).url(forResource: "cache", withExtension: "db")
|
Bundle(for: TestDbBuilder.self).url(forResource: "cache", withExtension: "db")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue