Merge pull request #216 from zcash/wallet-history

Wallet history improvements
This commit is contained in:
Francisco Gindre 2020-10-30 10:42:13 -03:00 committed by GitHub
commit 6db13ed19d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 430 additions and 30 deletions

4
Cargo.lock generated
View File

@ -1414,9 +1414,9 @@ dependencies = [
[[package]]
name = "zcash_client_sqlite"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc426de6690b40384ea0f8037a7fa0924c49905dcadc29a504fba2c9aefee4c4"
checksum = "58570135d82cb5c5df7aead33ef4a8963320cffcb42d17e7b61a3f86892fa7d7"
dependencies = [
"bech32",
"bs58",

View File

@ -12,7 +12,7 @@ failure = "0.1"
ffi_helpers = "0.2"
hex = "0.4"
zcash_client_backend = "0.4"
zcash_client_sqlite = "0.2"
zcash_client_sqlite = "0.2.1"
zcash_primitives = "0.4"
#### Temporary additions: ####################################

View File

@ -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)}
}
}

View File

@ -18,7 +18,7 @@ class CompactBlockEnhancementOperation: ZcashOperation {
override var isAsynchronous: Bool { false }
var rustBackend: ZcashRustBackendWelding.Type
var txFoundHandler: (([TransactionEntity]) -> Void)?
var txFoundHandler: (([ConfirmedTransactionEntity], BlockRange) -> Void)?
var downloader: CompactBlockDownloading
var repository: TransactionRepository
var maxRetries: Int = 5
@ -72,8 +72,8 @@ class CompactBlockEnhancementOperation: ZcashOperation {
return
}
if let handler = self.txFoundHandler, let foundTxs = try? repository.findTransactions(in: self.range, limit: Int.max) {
handler(foundTxs)
if let handler = self.txFoundHandler, let foundTxs = try? repository.findConfirmedTransactions(in: self.range, offset: 0, limit: Int.max) {
handler(foundTxs, self.range)
}
}

View File

@ -35,6 +35,7 @@ public struct CompactBlockProcessorNotificationKey {
public static let latestScannedBlockHeight = "CompactBlockProcessorNotificationKey.latestScannedBlockHeight"
public static let rewindHeight = "CompactBlockProcessorNotificationKey.rewindHeight"
public static let foundTransactions = "CompactBlockProcessorNotificationKey.foundTransactions"
public static let foundTransactionsRange = "CompactBlockProcessorNotificationKey.foundTransactionsRange"
public static let error = "error"
}
@ -91,7 +92,7 @@ public extension Notification.Name {
/**
Notification sent when the compact block processor enhanced a bunch of transactions
Query the user info object for CompactBlockProcessorNotificationKey.foundTransactions which will contain an [TransactionEntity] Array with the found transactions
Query the user info object for CompactBlockProcessorNotificationKey.foundTransactions which will contain an [ConfirmedTransactionEntity] Array with the found transactions and CompactBlockProcessorNotificationKey.foundTransactionsrange
*/
static let blockProcessorFoundTransactions = Notification.Name(rawValue: "CompactBlockProcessorFoundTransactions")
}
@ -289,6 +290,7 @@ public class CompactBlockProcessor {
// try validateConfiguration()
if retry {
self.retryAttempts = 0
self.processingError = nil
}
guard !queue.isSuspended else {
queue.isSuspended = false
@ -481,8 +483,8 @@ public class CompactBlockProcessor {
LoggerProxy.debug("Started Enhancing range: \(range)")
}
enhanceOperation.txFoundHandler = { [weak self] txs in
self?.notifyTransactions(txs)
enhanceOperation.txFoundHandler = { [weak self] (txs,range) in
self?.notifyTransactions(txs,in: range)
}
enhanceOperation.completionHandler = { [weak self] (finished, cancelled) in
@ -532,10 +534,12 @@ public class CompactBlockProcessor {
CompactBlockProcessorNotificationKey.progressHeight : self.latestBlockHeight])
}
func notifyTransactions(_ txs: [TransactionEntity]) {
func notifyTransactions(_ txs: [ConfirmedTransactionEntity], in range: BlockRange) {
NotificationCenter.default.post(name: .blockProcessorFoundTransactions,
object: self,
userInfo: [ CompactBlockProcessorNotificationKey.foundTransactions : txs])
userInfo: [ CompactBlockProcessorNotificationKey.foundTransactions : txs,
CompactBlockProcessorNotificationKey.foundTransactionsRange : ClosedRange(uncheckedBounds: (range.start.height,range.end.height))
])
}
private func validationFailed(at height: BlockHeight) {
@ -639,13 +643,22 @@ public class CompactBlockProcessor {
queue.cancelAllOperations()
// update retries
self.retryAttempts = self.retryAttempts + 1
self.processingError = nil
guard self.retryAttempts < config.retries else {
self.notifyError(CompactBlockProcessorError.maxAttemptsReached(attempts: self.retryAttempts))
self.stop()
return
}
processNewBlocks(range: range)
do {
try downloader.rewind(to: max(range.lowerBound, self.config.walletBirthday))
// process next batch
processNewBlocks(range: self.nextBatchBlockRange(latestHeight: latestBlockHeight, latestDownloadedHeight: try downloader.lastDownloadedBlockHeight()))
} catch {
self.fail(error)
}
}
func fail(_ error: Error) {
@ -690,6 +703,9 @@ public class CompactBlockProcessor {
}
// TODO: encapsulate service errors better
func mapError(_ error: Error) -> CompactBlockProcessorError {
if let processorError = error as? CompactBlockProcessorError {
return processorError
}
if let lwdError = error as? LightWalletServiceError {
return lwdError.mapToProcessorError()
} else if let rpcError = error as? GRPC.GRPCStatus {

View File

@ -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,
@ -224,6 +273,52 @@ class TransactionSQLDAO: TransactionRepository {
return tx
})
}
func findConfirmedTransactions(in range: BlockRange, offset: Int = 0, limit: Int = Int.max) throws -> [ConfirmedTransactionEntity]? {
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 (\(range.start.height) <= minedheight
AND minedheight <= \(range.end.height)) 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) OFFSET \(offset)
""").compactMap({ (bindings) -> ConfirmedTransactionEntity? in
guard let tx = TransactionBuilder.createConfirmedTransaction(from: bindings) else {
return nil
}
return tx
})
}
}
extension Data {

View File

@ -135,4 +135,6 @@ public extension ConfirmedTransactionEntity {
var blockTimeInMilliseconds: Double {
self.blockTimeInSeconds * 1000
}
}

View File

@ -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
}

View File

@ -19,8 +19,10 @@ 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?
func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]?
func findConfirmedTransactions(in range: BlockRange, offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]?
}

View File

@ -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() {

View File

@ -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:

View File

@ -352,19 +352,26 @@ public extension WalletBirthday {
time: 1585012771,
tree: "01e3bd906376b563d184bfbf6616e220f895001b7ef9d26bf38c6cb5c71e57a42b001001d848adf8c38d113140bb30d306b0761da6987e25ffc0d82faa63c2764aab120301de3e6a35d09192cde3430860c70a534d7b63e95a726fab052de2a9befa3cc3320189b958fa030131bb83385a3e3a8b187a166dc1b3a02050f2d2fc20788536c30e0001cb8770ef198e7de60093a339afbc561c16c16749f9f96751c2fc58a22d0ff36f01f86ff70dd512f7075d02c5ee6e28a8824832d08025a4cfaf4c1854f1fba5da10019bcac1b44a27de2c4528fa6f4b3432913511b219cb3b29d137cac0236a3d244800000000016f6df9b95ef63866bdf0e8b4b97701cd09232ec3e4e240808c0546d01bc7bb0501e3ec5d790cc9acc2586fc6e9ce5aae5f5aba32d33e386165c248c4a03ec8ed670000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
)
case 900000 ..< BlockHeight.max:
case 950000 ..< 1130000:
return WalletBirthday(
height: 950000,
hash: "0005050d2ce31b9b925c7ef4ab3d3166d5833bdcfee251294f29072a2bc0f75d",
time: 1591609525,
tree: "01f0637235c4a699d49ba996457a6c4eb7c67edd8270948065683deb19ef218363019f65a9692cefc7b90b42c1538ac1f38f7a7598549089c4561315b482f378523010000000000000018d30d0039277b05ab9e0c3990d53037c45892bf17af2d04fef40ed48c164ad2201ff5d86bbbe360e31378e783b740f8b05db2cf4246b95aa3851d22ed45554750300010cefb25743d5dd6062ef3afba438731cd5b35befc1038ecca3076fd205829e550001c19052386d8bbe3c07a1faf302281d67946cc9547e7e1890ff56b3a3ec69c0310001be53a6cd33da0442c7c6362c0172241f42e13c6dc893436a661a1cbf49775c1f00011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
)
case 1130000 ..< BlockHeight.max:
return WalletBirthday(
height: 1130000,
hash: "001f6563cf2863d36501dee00e41795f2fdb482970ea5f648791bbf7da4e6860",
time: 1603070117,
tree: "01b00c3e6d98e706fdb2d40f082096505aaa70fb87c067baf1a8a6d25cccff7258015a5334276509c93b855db0c2a1252e2ca725821c9274add1c2e92631bbbfa12b100151ba8342564941385670cd7346dc753bb5cc61164f000f65044cc09baa175917019ed834b8b9c8ca58969b3f239d74c2dd4a7d0a462afb1e856cf76209b866eb660135de9af32480f38eeca478a33a95f495abe7470e93d5aa4813fab7ea4fd12c4c0000017ce3fc7ebc2f8cfc2e1ee9f8e92b6e45065679ee3b48b5b2f0ce053305f5a95c011f1e8fe8f4a1cb5a700614218b8bebf2113c8a660abd255f67448b684b82d76e00000000016d559de7a1a382349cf97fe01a2fba41a49bb5e3b306d9ff8c2bcc301c731c00000001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
)
default:
return WalletBirthday(
height: 950000,
hash: "0005050d2ce31b9b925c7ef4ab3d3166d5833bdcfee251294f29072a2bc0f75d",
time: 1591609525,
tree: "01f0637235c4a699d49ba996457a6c4eb7c67edd8270948065683deb19ef218363019f65a9692cefc7b90b42c1538ac1f38f7a7598549089c4561315b482f378523010000000000000018d30d0039277b05ab9e0c3990d53037c45892bf17af2d04fef40ed48c164ad2201ff5d86bbbe360e31378e783b740f8b05db2cf4246b95aa3851d22ed45554750300010cefb25743d5dd6062ef3afba438731cd5b35befc1038ecca3076fd205829e550001c19052386d8bbe3c07a1faf302281d67946cc9547e7e1890ff56b3a3ec69c0310001be53a6cd33da0442c7c6362c0172241f42e13c6dc893436a661a1cbf49775c1f00011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
height: 1130000,
hash: "001f6563cf2863d36501dee00e41795f2fdb482970ea5f648791bbf7da4e6860",
time: 1603070117,
tree: "01b00c3e6d98e706fdb2d40f082096505aaa70fb87c067baf1a8a6d25cccff7258015a5334276509c93b855db0c2a1252e2ca725821c9274add1c2e92631bbbfa12b100151ba8342564941385670cd7346dc753bb5cc61164f000f65044cc09baa175917019ed834b8b9c8ca58969b3f239d74c2dd4a7d0a462afb1e856cf76209b866eb660135de9af32480f38eeca478a33a95f495abe7470e93d5aa4813fab7ea4fd12c4c0000017ce3fc7ebc2f8cfc2e1ee9f8e92b6e45065679ee3b48b5b2f0ce053305f5a95c011f1e8fe8f4a1cb5a700614218b8bebf2113c8a660abd255f67448b684b82d76e00000000016d559de7a1a382349cf97fe01a2fba41a49bb5e3b306d9ff8c2bcc301c731c00000001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
)
}
}

View File

@ -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
*/

View File

@ -48,10 +48,17 @@ public extension Notification.Name {
*/
static let synchronizerSyncing = Notification.Name("SDKSyncronizerSyncing")
/**
Posted when the synchronizer finds a mined transaction
Posted when the synchronizer finds a pendingTransaction that hast been newly mined
- Note: query userInfo on NotificationKeys.minedTransaction for the transaction
*/
static let synchronizerMinedTransaction = Notification.Name("synchronizerMinedTransaction")
/**
Posted when the synchronizer finds a mined transaction
- Note: query userInfo on NotificationKeys.foundTransactions for the [ConfirmedTransactionEntity]. This notification could arrive in a background thread.
*/
static let synchronizerFoundTransactions = Notification.Name("synchronizerFoundTransactions")
/**
Posted when the synchronizer presents an error
- Note: query userInfo on NotificationKeys.error for an error
@ -63,11 +70,12 @@ 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"
public static let minedTransaction = "SDKSynchronizer.minedTransaction"
public static let foundTransactions = "SDKSynchronizer.foundTransactions"
public static let error = "SDKSynchronizer.error"
}
@ -267,10 +275,23 @@ public class SDKSynchronizer: Synchronizer {
name: Notification.Name.blockProcessorHandledReOrg,
object: processor)
center.addObserver(self,
selector: #selector(transactionsFound(_:)),
name: Notification.Name.blockProcessorFoundTransactions,
object: processor)
}
// MARK: Block Processor notifications
@objc func transactionsFound(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let foundTransactions = userInfo[CompactBlockProcessorNotificationKey.foundTransactions] as? [ConfirmedTransactionEntity] else {
return
}
NotificationCenter.default.post(name: .synchronizerFoundTransactions, object: self, userInfo: [ NotificationKeys.foundTransactions : foundTransactions])
}
@objc func reorgDetected(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let progress = userInfo[CompactBlockProcessorNotificationKey.reorgHeight] as? BlockHeight,
@ -467,6 +488,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)
}

View File

@ -1125,6 +1125,34 @@ class AdvancedReOrgTests: XCTestCase {
}
func testLongSync() throws {
hookToReOrgNotification()
/*
1. create fake chain
*/
let fullSyncLength = 100_000
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, length: fullSyncLength)
try coordinator.applyStaged(blockheight: birthday + fullSyncLength)
sleep(10)
let firstSyncExpectation = XCTestExpectation(description: "first sync")
/*
sync to latest height
*/
try coordinator.sync(completion: { (s) in
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 300)
XCTAssertEqual(try coordinator.synchronizer.latestDownloadedHeight(), birthday + fullSyncLength)
}
func handleError(_ error: Error?) {
_ = try? coordinator.stop()
guard let testError = error else {
@ -1137,4 +1165,5 @@ class AdvancedReOrgTests: XCTestCase {
func hookToReOrgNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(handleReorg(_:)), name: .blockProcessorHandledReOrg, object: nil)
}
}

View File

@ -0,0 +1,138 @@
//
// SychronizerDarksideTests.swift
// ZcashLightClientKit-Unit-Tests
//
// Created by Francisco Gindre on 10/20/20.
//
import XCTest
@testable import ZcashLightClientKit
class SychronizerDarksideTests: XCTestCase {
var seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" //TODO: Parameterize this from environment?
let testRecipientAddress = "zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a" //TODO: Parameterize this from environment
let sendAmount: Int64 = 1000
var birthday: BlockHeight = 663150
let defaultLatestHeight: BlockHeight = 663175
var coordinator: TestCoordinator!
var syncedExpectation = XCTestExpectation(description: "synced")
var sentTransactionExpectation = XCTestExpectation(description: "sent")
var expectedReorgHeight: BlockHeight = 665188
var expectedRewindHeight: BlockHeight = 665188
var reorgExpectation: XCTestExpectation = XCTestExpectation(description: "reorg")
var foundTransactions = [ConfirmedTransactionEntity]()
override func setUpWithError() throws {
coordinator = try TestCoordinator(
seed: seedPhrase,
walletBirthday: birthday,
channelProvider: ChannelProvider()
)
try coordinator.reset(saplingActivation: 663150)
}
override func tearDownWithError() throws {
NotificationCenter.default.removeObserver(self)
try coordinator.stop()
try? FileManager.default.removeItem(at: coordinator.databases.cacheDB)
try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
try? FileManager.default.removeItem(at: coordinator.databases.pendingDB)
}
func testFoundTransactions() throws {
NotificationCenter.default.addObserver(self, selector: #selector(handleFoundTransactions(_:)), name: Notification.Name.synchronizerFoundTransactions, object: nil)
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service)
let receivedTxHeight: BlockHeight = 663188
try coordinator.applyStaged(blockheight: receivedTxHeight + 1)
sleep(2)
let preTxExpectation = XCTestExpectation(description: "pre receive")
try coordinator.sync(completion: { (synchronizer) in
preTxExpectation.fulfill()
}, error: self.handleError)
wait(for: [preTxExpectation], timeout: 5)
XCTAssertEqual(self.foundTransactions.count, 2)
}
func testFoundManyTransactions() throws {
NotificationCenter.default.addObserver(self, selector: #selector(handleFoundTransactions(_:)), name: Notification.Name.synchronizerFoundTransactions, object: nil)
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, length: 1000)
let receivedTxHeight: BlockHeight = 663229
try coordinator.applyStaged(blockheight: receivedTxHeight + 1)
sleep(2)
let firsTxExpectation = XCTestExpectation(description: "first sync")
try coordinator.sync(completion: { (synchronizer) in
firsTxExpectation.fulfill()
}, error: self.handleError)
wait(for: [firsTxExpectation], timeout: 10)
XCTAssertEqual(self.foundTransactions.count, 5)
self.foundTransactions.removeAll()
try coordinator.applyStaged(blockheight: 663900)
sleep(2)
let preTxExpectation = XCTestExpectation(description: "intermediate sync")
try coordinator.sync(completion: { (synchronizer) in
preTxExpectation.fulfill()
}, error: self.handleError)
wait(for: [preTxExpectation], timeout: 10)
XCTAssertTrue(self.foundTransactions.count == 0)
let findManyTxExpectation = XCTestExpectation(description: "final sync")
try coordinator.applyStaged(blockheight: 664010)
sleep(2)
try coordinator.sync(completion: { (synchronizer) in
findManyTxExpectation.fulfill()
}, error: self.handleError)
wait(for: [findManyTxExpectation], timeout: 10)
XCTAssertEqual(self.foundTransactions.count, 2)
}
@objc func handleFoundTransactions(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let transactions = userInfo[SDKSynchronizer.NotificationKeys.foundTransactions] as? [ConfirmedTransactionEntity] else {
return
}
self.foundTransactions.append(contentsOf: transactions)
}
func handleError(_ error: Error?) {
_ = try? coordinator.stop()
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
}
}

View File

@ -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 {

View File

@ -26,6 +26,22 @@ class FakeChainBuilder {
}
static func buildChain(darksideWallet: DarksideWalletService, length: Int) throws {
try darksideWallet.reset(saplingActivation: 663150)
try darksideWallet.useDataset(from: txMainnetBlockUrl)
try darksideWallet.stageBlocksCreate(from: 663151, count: length)
try darksideWallet.stageTransaction(from: txUrls[663174]!, at: 663174)
try darksideWallet.stageTransaction(from: txUrls[663188]!, at: 663188)
try darksideWallet.stageTransaction(from: txUrls[663202]!, at: 663202)
try darksideWallet.stageTransaction(from: txUrls[663218]!, at: 663218)
try darksideWallet.stageTransaction(from: txUrls[663229]!, at: 663229)
try darksideWallet.stageTransaction(from: txUrls[663953]!, at: 663953)
try darksideWallet.stageTransaction(from: txUrls[663974]!, at: 663974)
}
static func buildTxUrl(for id: String) -> String {
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/\(id).txt"
}

View File

@ -10,14 +10,18 @@ import Foundation
@testable import ZcashLightClientKit
class MockTransactionRepository: TransactionRepository {
func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? {
func findConfirmedTransactions(in range: BlockRange, offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
nil
}
// func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? {
// nil
// }
func findAll(from: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? {
nil
}
func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? {
nil
}
var unminedCount: Int
var receivedCount: Int