From 972fb089260a533d8eac9fd5d4ceedafd346a29c Mon Sep 17 00:00:00 2001 From: Francisco Gindre Date: Mon, 19 Oct 2020 21:01:46 -0300 Subject: [PATCH] Issue 208 - Improve API method to request transaction history --- .../TransactionsDataSource.swift | 4 +- ZcashLightClientKit/DAO/TransactionDao.swift | 49 +++++++++++++++ .../Entity/TransactionEntity.swift | 2 + ZcashLightClientKit/Initializer.swift | 1 - .../Repository/TransactionRepository.swift | 1 + .../Rust/ZcashRustBackend.swift | 4 +- .../Rust/ZcashRustBackendWelding.swift | 1 - ZcashLightClientKit/Synchronizer.swift | 10 ++++ .../UIKit/Synchronizer/SDKSynchronizer.swift | 6 +- .../TransactionRepositoryTests.swift | 60 +++++++++++++++++++ .../utils/MockTransactionRepository.swift | 4 ++ 11 files changed, 134 insertions(+), 8 deletions(-) diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/List Transactions/TransactionsDataSource.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/List Transactions/TransactionsDataSource.swift index b9fb97a1..427b2839 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/List Transactions/TransactionsDataSource.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/List Transactions/TransactionsDataSource.swift @@ -65,9 +65,7 @@ class TransactionsDataSource: NSObject, UITableViewDataSource { } case .all: transactions = (synchronizer.pendingTransactions.map { $0.transactionEntity } + - synchronizer.clearedTransactions.map { $0.transactionEntity } + - synchronizer.receivedTransactions.map { $0.transactionEntity } + - synchronizer.sentTransactions.map { $0.transactionEntity }).map { TransactionDetailModel(transaction: $0)} + synchronizer.clearedTransactions.map { $0.transactionEntity }).map { TransactionDetailModel(transaction: $0)} } } diff --git a/ZcashLightClientKit/DAO/TransactionDao.swift b/ZcashLightClientKit/DAO/TransactionDao.swift index 96aa34a2..81702772 100644 --- a/ZcashLightClientKit/DAO/TransactionDao.swift +++ b/ZcashLightClientKit/DAO/TransactionDao.swift @@ -202,6 +202,55 @@ class TransactionSQLDAO: TransactionRepository { }) } + func findAll(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? { + guard let fromTransaction = transaction else { + return try findAll(offset: 0, limit: limit) + } + + return try dbProvider.connection().run(""" + SELECT transactions.id_tx AS id, + transactions.block AS minedHeight, + transactions.tx_index AS transactionIndex, + transactions.txid AS rawTransactionId, + transactions.expiry_height AS expiryHeight, + transactions.raw AS raw, + sent_notes.address AS toAddress, + CASE + WHEN sent_notes.value IS NOT NULL THEN sent_notes.value + ELSE received_notes.value + end AS value, + CASE + WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo + ELSE received_notes.memo + end AS memo, + CASE + WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note + ELSE received_notes.id_note + end AS noteId, + blocks.time AS blockTimeInSeconds + FROM transactions + LEFT JOIN received_notes + ON transactions.id_tx = received_notes.tx + LEFT JOIN sent_notes + ON transactions.id_tx = sent_notes.tx + LEFT JOIN blocks + ON transactions.block = blocks.height + WHERE (\(fromTransaction.blockTimeInSeconds), \(fromTransaction.transactionIndex)) > (blocktimeinseconds, transactionIndex) AND + (sent_notes.address IS NULL AND received_notes.is_change != 1) + OR sent_notes.address IS NOT NULL + ORDER BY ( minedheight IS NOT NULL ), + minedheight DESC, + blocktimeinseconds DESC, + id DESC + LIMIT \(limit) + """).compactMap({ (bindings) -> ConfirmedTransactionEntity? in + guard let tx = TransactionBuilder.createConfirmedTransaction(from: bindings) else { + return nil + } + return tx + }) + } + func findTransactions(in range: BlockRange, limit: Int = Int.max) throws -> [TransactionEntity]? { try dbProvider.connection().run(""" SELECT transactions.id_tx AS id, diff --git a/ZcashLightClientKit/Entity/TransactionEntity.swift b/ZcashLightClientKit/Entity/TransactionEntity.swift index 50e3ca46..402db09d 100644 --- a/ZcashLightClientKit/Entity/TransactionEntity.swift +++ b/ZcashLightClientKit/Entity/TransactionEntity.swift @@ -135,4 +135,6 @@ public extension ConfirmedTransactionEntity { var blockTimeInMilliseconds: Double { self.blockTimeInSeconds * 1000 } + } + diff --git a/ZcashLightClientKit/Initializer.swift b/ZcashLightClientKit/Initializer.swift index 263abe3b..115c17a6 100644 --- a/ZcashLightClientKit/Initializer.swift +++ b/ZcashLightClientKit/Initializer.swift @@ -194,7 +194,6 @@ public class Initializer { transactionRepository: transactionRepository, backend: rustBackend) - guard try rustBackend.initAccountsTable(dbData: dataDbURL, exfvks: viewingKeys) else { throw rustBackend.lastError() ?? InitializerError.accountInitFailed } diff --git a/ZcashLightClientKit/Repository/TransactionRepository.swift b/ZcashLightClientKit/Repository/TransactionRepository.swift index 7b9496eb..74054676 100644 --- a/ZcashLightClientKit/Repository/TransactionRepository.swift +++ b/ZcashLightClientKit/Repository/TransactionRepository.swift @@ -19,6 +19,7 @@ protocol TransactionRepository { func findAllSentTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? func findAllReceivedTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? func findAll(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? + func findAll(from: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? func lastScannedHeight() throws -> BlockHeight func isInitialized() throws -> Bool func findEncodedTransactionBy(txId: Int) -> EncodedTransaction? diff --git a/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 9937f7dd..b2054fd2 100644 --- a/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -92,7 +92,7 @@ class ZcashRustBackend: ZcashRustBackendWelding { static func initAccountsTable(dbData: URL, exfvks: [String]) throws -> Bool { let dbData = dbData.osStr() - let viewingKeys = exfvks.map { UnsafePointer(strdup($0)) } + let viewingKeys = exfvks.map { UnsafePointer(strdup($0)) } guard exfvks.count > 0 else { throw RustWeldingError.malformedStringInput @@ -100,7 +100,7 @@ class ZcashRustBackend: ZcashRustBackendWelding { let res = zcashlc_init_accounts_table_with_keys(dbData.0, dbData.1, viewingKeys, UInt(viewingKeys.count)); - viewingKeys.compactMap({UnsafeMutablePointer(mutating: $0)}).forEach({ free($0) }) + viewingKeys.compactMap({ UnsafeMutablePointer(mutating: $0) }).forEach({ free($0) }) guard res else { if let error = lastError() { diff --git a/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index b7090b3a..5fc67b01 100644 --- a/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -57,7 +57,6 @@ public protocol ZcashRustBackendWelding { */ static func initAccountsTable(dbData: URL, seed: [UInt8], accounts: Int32) -> [String]? - /** initialize the accounts table from a given seed and a number of accounts - Parameters: diff --git a/ZcashLightClientKit/Synchronizer.swift b/ZcashLightClientKit/Synchronizer.swift index 4a0ad28d..f740a386 100644 --- a/ZcashLightClientKit/Synchronizer.swift +++ b/ZcashLightClientKit/Synchronizer.swift @@ -105,6 +105,16 @@ public protocol Synchronizer { */ func paginatedTransactions(of kind: TransactionKind) -> PaginatedTransactionRepository + /** + Returns a list of confirmed transactions that preceed the given transaction with a limit count. + - Parameters: + - from: the confirmed transaction from which the query should start from or nil to retrieve from the most recent transaction + - limit: the maximum amount of items this should return if available + - Returns: an array with the given Transactions or nil + + */ + func allConfirmedTransactions(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? + /** gets the latest downloaded height from the compact block cache */ diff --git a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift index 6d7ec619..ebbdc2d6 100644 --- a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift +++ b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift @@ -63,7 +63,7 @@ public extension Notification.Name { Synchronizer implementation for UIKit and iOS 12+ */ public class SDKSynchronizer: Synchronizer { - + public struct NotificationKeys { public static let progress = "SDKSynchronizer.progress" public static let blockHeight = "SDKSynchronizer.blockHeight" @@ -467,6 +467,10 @@ public class SDKSynchronizer: Synchronizer { try transactionRepository.findAllSentTransactions(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]() } + public func allConfirmedTransactions(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? { + try transactionRepository.findAll(from: transaction, limit: limit) + } + public func paginatedTransactions(of kind: TransactionKind = .all) -> PaginatedTransactionRepository { PagedTransactionRepositoryBuilder.build(initializer: initializer, kind: .all) } diff --git a/ZcashLightClientKitTests/TransactionRepositoryTests.swift b/ZcashLightClientKitTests/TransactionRepositoryTests.swift index 1259f89e..22736856 100644 --- a/ZcashLightClientKitTests/TransactionRepositoryTests.swift +++ b/ZcashLightClientKitTests/TransactionRepositoryTests.swift @@ -109,6 +109,66 @@ class TransactionRepositoryTests: XCTestCase { } } } + + func testFindAllFrom() throws { + guard let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max), + let allFromNil = try self.transactionRepository.findAll(from: nil, limit: Int.max) + else { + return XCTFail("find all failed") + } + + XCTAssertEqual(transactions.count, allFromNil.count) + + for t in transactions { + guard allFromNil.first(where: { $0.rawTransactionId == t.rawTransactionId}) != nil else { + XCTFail("not equal") + return + } + } + } + + func testFindAllFromSlice() throws { + + let limit = 4 + let start = 7 + guard let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max), + let allFromNil = try self.transactionRepository.findAll(from: transactions[start], limit: limit) + else { + return XCTFail("find all failed") + } + + XCTAssertEqual(limit, allFromNil.count) + + let slice = transactions[start + 1 ... start + limit] + XCTAssertEqual(slice.count, allFromNil.count) + for t in slice { + guard allFromNil.first(where: { $0.rawTransactionId == t.rawTransactionId}) != nil else { + XCTFail("not equal") + return + } + } + } + + + func testFindAllFromLastSlice() throws { + + let limit = 10 + let start = 20 + guard let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max), + let allFromNil = try self.transactionRepository.findAll(from: transactions[start], limit: limit) + else { + return XCTFail("find all failed") + } + + let slice = transactions[start + 1 ..< transactions.count] + XCTAssertEqual(slice.count, allFromNil.count) + for t in slice { + guard allFromNil.first(where: { $0.rawTransactionId == t.rawTransactionId}) != nil else { + XCTFail("not equal") + return + } + } + } } extension Data { diff --git a/ZcashLightClientKitTests/utils/MockTransactionRepository.swift b/ZcashLightClientKitTests/utils/MockTransactionRepository.swift index 43d39d5b..d1ae8e9f 100644 --- a/ZcashLightClientKitTests/utils/MockTransactionRepository.swift +++ b/ZcashLightClientKitTests/utils/MockTransactionRepository.swift @@ -10,6 +10,10 @@ import Foundation @testable import ZcashLightClientKit class MockTransactionRepository: TransactionRepository { + func findAll(from: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? { + nil + } + func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? { nil }