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 closeFailed
|
||||
case operationFailed
|
||||
case updateFailed
|
||||
case malformedEntity(fields: [String]?)
|
||||
}
|
||||
|
|
|
@ -6,37 +6,179 @@
|
|||
//
|
||||
|
||||
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 {
|
||||
|
||||
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
|
||||
|
||||
init(dbProvider: ConnectionProvider) {
|
||||
self.dbProvider = dbProvider
|
||||
}
|
||||
|
||||
func create(_ transaction: PendingTransactionEntity) throws -> Int64 {
|
||||
-1
|
||||
func createrTableIfNeeded() throws {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
|
||||
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? {
|
||||
nil
|
||||
func find(by id: Int) throws -> PendingTransactionEntity? {
|
||||
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] {
|
||||
[]
|
||||
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),
|
||||
transactionIndex: Int(transactionIndex),
|
||||
raw: raw,
|
||||
id: UInt(id),
|
||||
id: Int(id),
|
||||
value: Int(value),
|
||||
memo: memo,
|
||||
rawTransactionId: transactionId)
|
||||
|
@ -110,7 +110,7 @@ struct TransactionBuilder {
|
|||
blockTimeInSeconds: TimeInterval(integerLiteral: blockTimeInSeconds),
|
||||
transactionIndex: Int(transactionIndex),
|
||||
raw: nil,
|
||||
id: UInt(id),
|
||||
id: Int(id),
|
||||
value: Int(value),
|
||||
memo: memo,
|
||||
rawTransactionId: transactionId)
|
||||
|
|
|
@ -36,7 +36,7 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
|
|||
var blockTimeInSeconds: TimeInterval
|
||||
var transactionIndex: Int
|
||||
var raw: Data?
|
||||
var id: UInt
|
||||
var id: Int?
|
||||
var value: Int
|
||||
var memo: Data?
|
||||
var rawTransactionId: Data?
|
||||
|
@ -45,12 +45,12 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
|
|||
class TransactionSQLDAO: TransactionRepository {
|
||||
|
||||
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 created = Expression<String?>(Transaction.CodingKeys.created.rawValue)
|
||||
static var txIndex = Expression<String?>(Transaction.CodingKeys.transactionIndex.rawValue)
|
||||
static var expiryHeight = Expression<Int64?>(Transaction.CodingKeys.expiryHeight.rawValue)
|
||||
static var minedHeight = Expression<Int64?>(Transaction.CodingKeys.minedHeight.rawValue)
|
||||
static var expiryHeight = Expression<Int?>(Transaction.CodingKeys.expiryHeight.rawValue)
|
||||
static var minedHeight = Expression<Int?>(Transaction.CodingKeys.minedHeight.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)
|
||||
}
|
||||
|
||||
func findBy(id: Int64) throws -> TransactionEntity? {
|
||||
let query = transactions.filter(TableStructure.id == Int64(id)).limit(1)
|
||||
func findBy(id: Int) throws -> TransactionEntity? {
|
||||
let query = transactions.filter(TableStructure.id == id).limit(1)
|
||||
let sequence = try dbProvider.connection().prepare(query)
|
||||
let entity: Transaction? = try sequence.map({ try $0.decode() }).first
|
||||
return entity
|
||||
|
|
|
@ -15,7 +15,7 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
|
|||
var cancelled: Int { get set }
|
||||
var encodeAttempts: Int { get set }
|
||||
var submitAttempts: Int { get set }
|
||||
var errorMesssage: String? { get set }
|
||||
var errorMessage: String? { get set }
|
||||
var errorCode: Int? { get set }
|
||||
var createTime: TimeInterval { get set }
|
||||
|
||||
|
@ -47,7 +47,7 @@ public extension PendingTransactionEntity {
|
|||
}
|
||||
|
||||
var isFailedSubmit: Bool {
|
||||
errorMesssage != nil || (errorCode != nil && (errorCode ?? 0) < 0)
|
||||
errorMessage != nil || (errorCode != nil && (errorCode ?? 0) < 0)
|
||||
}
|
||||
|
||||
var isFailure: Bool {
|
||||
|
@ -72,6 +72,6 @@ public extension PendingTransactionEntity {
|
|||
}
|
||||
|
||||
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 {
|
||||
var id: UInt { get set }
|
||||
var id: Int? { get set }
|
||||
var value: Int { get set }
|
||||
var memo: Data? { get set }
|
||||
}
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import Foundation
|
||||
|
||||
protocol PendingTransactionRepository {
|
||||
func create(_ transaction: PendingTransactionEntity) throws -> Int64
|
||||
func create(_ transaction: PendingTransactionEntity) throws -> Int
|
||||
func update(_ transaction: PendingTransactionEntity) throws
|
||||
func delete(_ 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]
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ enum TransactionRepositoryError: Error {
|
|||
protocol TransactionRepository {
|
||||
func countAll() 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 findAllSentTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?
|
||||
func findAllReceivedTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?
|
||||
|
|
|
@ -51,6 +51,24 @@ public class LightWalletGRPCService {
|
|||
}
|
||||
|
||||
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] {
|
||||
var blocks = [ZcashCompactBlock]()
|
||||
|
@ -142,3 +160,4 @@ extension LightWalletGRPCService: LightWalletService {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
|
||||
import Foundation
|
||||
import SwiftGRPC
|
||||
|
||||
import SwiftProtobuf
|
||||
public enum LightWalletServiceError: Error {
|
||||
case generalError
|
||||
case failed(statusCode: StatusCode)
|
||||
case invalidBlock
|
||||
case sentFailed(sendResponse: LightWalletServiceResponse)
|
||||
case genericError(error: Error)
|
||||
}
|
||||
|
||||
extension LightWalletServiceError: Equatable {
|
||||
|
@ -40,11 +42,28 @@ extension LightWalletServiceError: Equatable {
|
|||
default:
|
||||
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 {
|
||||
/**
|
||||
Return the latest block height known to the service.
|
||||
|
@ -79,4 +98,16 @@ public protocol LightWalletService {
|
|||
*/
|
||||
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
|
||||
|
||||
public enum TransactionEncoderError: Error {
|
||||
case notFound(transactionId: Int64)
|
||||
case NotEncoded(transactionId: Int64)
|
||||
case notFound(transactionId: Int)
|
||||
case NotEncoded(transactionId: Int)
|
||||
case missingParams
|
||||
case spendingKeyWrongNetwork
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ protocol TransactionEncoder {
|
|||
double-bangs for things).
|
||||
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
|
||||
|
@ -32,6 +32,6 @@ protocol TransactionEncoder {
|
|||
double-bangs for things).
|
||||
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
|
||||
|
||||
class WalletTransactionEncoder: TransactionEncoder {
|
||||
|
||||
var rustBackend: ZcashRustBackend.Type
|
||||
var repository: TransactionRepository
|
||||
var initializer: Initializer
|
||||
var queue: DispatchQueue
|
||||
init(rust: ZcashRustBackend.Type, repository: TransactionRepository, initializer: Initializer) {
|
||||
self.rustBackend = rust
|
||||
self.repository = repository
|
||||
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)
|
||||
|
||||
|
@ -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 }
|
||||
do {
|
||||
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),
|
||||
let spend = URL(string: initializer.spendParamsURL.path), let output = URL(string: initializer.outputParamsURL.path) else {
|
||||
throw TransactionEncoderError.missingParams
|
||||
|
@ -60,7 +63,7 @@ class WalletTransactionEncoder: TransactionEncoder {
|
|||
throw rustBackend.lastError() ?? RustWeldingError.genericError(message: "create spend failed")
|
||||
}
|
||||
|
||||
return txId
|
||||
return Int(txId)
|
||||
}
|
||||
|
||||
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!
|
||||
let spendingKey = "secret-extended-key-test1qvpevftsqqqqpqy52ut2vv24a2qh7nsukew7qg9pq6djfwyc3xt5vaxuenshp2hhspp9qmqvdh0gs2ljpwxders5jkwgyhgln0drjqaguaenfhehz4esdl4kwlm5t9q0l6wmzcrvcf5ed6dqzvct3e2ge7f6qdvzhp02m7sp5a0qjssrwpdh7u6tq89hl3wchuq8ljq8r8rwd6xdwh3nry9at80z7amnj3s6ah4jevnvfr08gxpws523z95g6dmn4wm6l3658kd4xcq9rc0qn"
|
||||
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
|
||||
let zpend: Int64 = 500_000
|
||||
let zpend: Int = 500_000
|
||||
|
||||
override func setUp() {
|
||||
try! dataDbHandle.setUp()
|
||||
|
@ -54,7 +54,7 @@ class WalletTransactionEncoderTests: XCTestCase {
|
|||
|
||||
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) }())
|
||||
|
||||
|
|
|
@ -11,16 +11,20 @@ import XCTest
|
|||
|
||||
class ZcashRustBackendTests: XCTestCase {
|
||||
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() {
|
||||
dbData = try! __dataDbURL()
|
||||
|
||||
try? dataDbHandle.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
||||
try? FileManager.default.removeItem(at: dbData!)
|
||||
|
||||
dataDbHandle.dispose()
|
||||
}
|
||||
|
||||
func testInitAndGetAddress() {
|
||||
|
@ -60,4 +64,11 @@ class ZcashRustBackendTests: XCTestCase {
|
|||
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 SwiftProtobuf
|
||||
@testable import ZcashLightClientKit
|
||||
|
||||
struct LightWalletServiceMockResponse: LightWalletServiceResponse {
|
||||
var errorCode: Int32
|
||||
var errorMessage: String
|
||||
var unknownFields: UnknownStorage
|
||||
}
|
||||
|
||||
class MockLightWalletService: LightWalletService {
|
||||
|
||||
private var service = LightWalletGRPCService(channel: ChannelProvider().channel())
|
||||
|
@ -34,5 +42,13 @@ class MockLightWalletService: LightWalletService {
|
|||
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 {
|
||||
|
|
|
@ -50,6 +50,9 @@ class TestDbBuilder {
|
|||
return compactBlockDao
|
||||
}
|
||||
|
||||
static func pendingTransactionsDbURL() throws -> URL {
|
||||
try __documentsDirectory().appendingPathComponent("pending.db")
|
||||
}
|
||||
static func prePopulatedCacheDbURL() -> URL? {
|
||||
Bundle(for: TestDbBuilder.self).url(forResource: "cache", withExtension: "db")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue