diff --git a/Zircles.xcodeproj/project.pbxproj b/Zircles.xcodeproj/project.pbxproj index 7f1a3cd..09298ce 100644 --- a/Zircles.xcodeproj/project.pbxproj +++ b/Zircles.xcodeproj/project.pbxproj @@ -54,6 +54,20 @@ 0DB5331224A6413D0090D722 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB5331124A6413D0090D722 /* LazyView.swift */; }; 0DB5331424A6B8B60090D722 /* memo.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB5331324A6B8B60090D722 /* memo.pb.swift */; }; 0DC1B47B24AAA44B00AEF3D0 /* ZircleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC1B47A24AAA44B00AEF3D0 /* ZircleService.swift */; }; + 0DC4B44A24ACB62400B3CBA2 /* darkside.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B43D24ACB62400B3CBA2 /* darkside.grpc.swift */; }; + 0DC4B44B24ACB62400B3CBA2 /* darkside.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B43E24ACB62400B3CBA2 /* darkside.pb.swift */; }; + 0DC4B44D24ACB62400B3CBA2 /* MockTransactionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44124ACB62400B3CBA2 /* MockTransactionRepository.swift */; }; + 0DC4B44E24ACB62400B3CBA2 /* SampleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44224ACB62400B3CBA2 /* SampleLogger.swift */; }; + 0DC4B44F24ACB62400B3CBA2 /* FakeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44324ACB62400B3CBA2 /* FakeService.swift */; }; + 0DC4B45024ACB62400B3CBA2 /* FakeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44424ACB62400B3CBA2 /* FakeStorage.swift */; }; + 0DC4B45124ACB62400B3CBA2 /* TestDbBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44524ACB62400B3CBA2 /* TestDbBuilder.swift */; }; + 0DC4B45224ACB62400B3CBA2 /* FakeChainBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44624ACB62400B3CBA2 /* FakeChainBuilder.swift */; }; + 0DC4B45324ACB62400B3CBA2 /* Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44724ACB62400B3CBA2 /* Stubs.swift */; }; + 0DC4B45424ACB62400B3CBA2 /* DarkSideWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44824ACB62400B3CBA2 /* DarkSideWalletService.swift */; }; + 0DC4B45524ACB62400B3CBA2 /* Tests+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B44924ACB62400B3CBA2 /* Tests+Utils.swift */; }; + 0DC4B45724ACF8EE00B3CBA2 /* TestCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4B45624ACF8EE00B3CBA2 /* TestCoordinator.swift */; }; + 0DC4B45824AD281100B3CBA2 /* sapling-output.params in Resources */ = {isa = PBXBuildFile; fileRef = 0DB5330E24A623AF0090D722 /* sapling-output.params */; }; + 0DC4B45924AD281400B3CBA2 /* sapling-spend.params in Resources */ = {isa = PBXBuildFile; fileRef = 0DB5330D24A623AF0090D722 /* sapling-spend.params */; }; 0DD7278924A5532300C36D27 /* AllZirclesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DD7278824A5532300C36D27 /* AllZirclesView.swift */; }; 0DD7278B24A564AF00C36D27 /* ZircleListCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DD7278A24A564AF00C36D27 /* ZircleListCard.swift */; }; 0DEE59A824A24B7300447C15 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DEE59A724A24B7300447C15 /* WelcomeView.swift */; }; @@ -132,6 +146,18 @@ 0DB5331124A6413D0090D722 /* LazyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 0DB5331324A6B8B60090D722 /* memo.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = memo.pb.swift; sourceTree = ""; }; 0DC1B47A24AAA44B00AEF3D0 /* ZircleService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZircleService.swift; sourceTree = ""; }; + 0DC4B43D24ACB62400B3CBA2 /* darkside.grpc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = darkside.grpc.swift; sourceTree = ""; }; + 0DC4B43E24ACB62400B3CBA2 /* darkside.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = darkside.pb.swift; sourceTree = ""; }; + 0DC4B44124ACB62400B3CBA2 /* MockTransactionRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTransactionRepository.swift; sourceTree = ""; }; + 0DC4B44224ACB62400B3CBA2 /* SampleLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleLogger.swift; sourceTree = ""; }; + 0DC4B44324ACB62400B3CBA2 /* FakeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeService.swift; sourceTree = ""; }; + 0DC4B44424ACB62400B3CBA2 /* FakeStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeStorage.swift; sourceTree = ""; }; + 0DC4B44524ACB62400B3CBA2 /* TestDbBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDbBuilder.swift; sourceTree = ""; }; + 0DC4B44624ACB62400B3CBA2 /* FakeChainBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeChainBuilder.swift; sourceTree = ""; }; + 0DC4B44724ACB62400B3CBA2 /* Stubs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stubs.swift; sourceTree = ""; }; + 0DC4B44824ACB62400B3CBA2 /* DarkSideWalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkSideWalletService.swift; sourceTree = ""; }; + 0DC4B44924ACB62400B3CBA2 /* Tests+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Tests+Utils.swift"; sourceTree = ""; }; + 0DC4B45624ACF8EE00B3CBA2 /* TestCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCoordinator.swift; sourceTree = ""; }; 0DD7278824A5532300C36D27 /* AllZirclesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllZirclesView.swift; sourceTree = ""; }; 0DD7278A24A564AF00C36D27 /* ZircleListCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZircleListCard.swift; sourceTree = ""; }; 0DEE59A724A24B7300447C15 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; @@ -266,6 +292,8 @@ 0D1366C124991A6200F0EB54 /* ZirclesTests */ = { isa = PBXGroup; children = ( + 0DC4B43C24ACB62400B3CBA2 /* proto */, + 0DC4B44024ACB62400B3CBA2 /* utils */, 0D1366C224991A6200F0EB54 /* ZirclesTests.swift */, 0D1366C424991A6200F0EB54 /* Info.plist */, ); @@ -337,6 +365,32 @@ path = ZircleService; sourceTree = ""; }; + 0DC4B43C24ACB62400B3CBA2 /* proto */ = { + isa = PBXGroup; + children = ( + 0DC4B43D24ACB62400B3CBA2 /* darkside.grpc.swift */, + 0DC4B43E24ACB62400B3CBA2 /* darkside.pb.swift */, + ); + path = proto; + sourceTree = ""; + }; + 0DC4B44024ACB62400B3CBA2 /* utils */ = { + isa = PBXGroup; + children = ( + 0DC4B45624ACF8EE00B3CBA2 /* TestCoordinator.swift */, + 0DC4B44124ACB62400B3CBA2 /* MockTransactionRepository.swift */, + 0DC4B44224ACB62400B3CBA2 /* SampleLogger.swift */, + 0DC4B44324ACB62400B3CBA2 /* FakeService.swift */, + 0DC4B44424ACB62400B3CBA2 /* FakeStorage.swift */, + 0DC4B44524ACB62400B3CBA2 /* TestDbBuilder.swift */, + 0DC4B44624ACB62400B3CBA2 /* FakeChainBuilder.swift */, + 0DC4B44724ACB62400B3CBA2 /* Stubs.swift */, + 0DC4B44824ACB62400B3CBA2 /* DarkSideWalletService.swift */, + 0DC4B44924ACB62400B3CBA2 /* Tests+Utils.swift */, + ); + path = utils; + sourceTree = ""; + }; E7393F4D108BAE27C875D7F0 /* Pods */ = { isa = PBXGroup; children = ( @@ -472,6 +526,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0DC4B45824AD281100B3CBA2 /* sapling-output.params in Resources */, + 0DC4B45924AD281400B3CBA2 /* sapling-spend.params in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -641,7 +697,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0DC4B45024ACB62400B3CBA2 /* FakeStorage.swift in Sources */, 0D1366C324991A6200F0EB54 /* ZirclesTests.swift in Sources */, + 0DC4B44F24ACB62400B3CBA2 /* FakeService.swift in Sources */, + 0DC4B44A24ACB62400B3CBA2 /* darkside.grpc.swift in Sources */, + 0DC4B44D24ACB62400B3CBA2 /* MockTransactionRepository.swift in Sources */, + 0DC4B45524ACB62400B3CBA2 /* Tests+Utils.swift in Sources */, + 0DC4B44B24ACB62400B3CBA2 /* darkside.pb.swift in Sources */, + 0DC4B45324ACB62400B3CBA2 /* Stubs.swift in Sources */, + 0DC4B45124ACB62400B3CBA2 /* TestDbBuilder.swift in Sources */, + 0DC4B45224ACB62400B3CBA2 /* FakeChainBuilder.swift in Sources */, + 0DC4B44E24ACB62400B3CBA2 /* SampleLogger.swift in Sources */, + 0DC4B45724ACF8EE00B3CBA2 /* TestCoordinator.swift in Sources */, + 0DC4B45424ACB62400B3CBA2 /* DarkSideWalletService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Zircles/CombineSynchronizer.swift b/Zircles/CombineSynchronizer.swift index 54db057..9aa8102 100644 --- a/Zircles/CombineSynchronizer.swift +++ b/Zircles/CombineSynchronizer.swift @@ -14,7 +14,7 @@ class CombineSynchronizer { var initializer: Initializer { synchronizer.initializer } - private var synchronizer: SDKSynchronizer + var synchronizer: SDKSynchronizer var status: CurrentValueSubject var progress: CurrentValueSubject diff --git a/Zircles/ZircleService/ZircleService.swift b/Zircles/ZircleService/ZircleService.swift index f901f25..f70ab9d 100644 --- a/Zircles/ZircleService/ZircleService.swift +++ b/Zircles/ZircleService/ZircleService.swift @@ -13,7 +13,7 @@ enum ZircleServiceError: Error { case generalError(message: String) } protocol ZircleService { - func createNewZircle(name: String, goal zatoshi: Int64, frequency: ZircleFrequency, endDate: ZircleEndDate) throws -> Future + func createNewZircle(name: String, goal zatoshi: Int64, frequency: ZircleFrequency, endDate: ZircleEndDate, spendingKey: String) throws -> Future func closeZircle(name: String) throws func contribute(zatoshi: Int64, zircle: ZircleEntity) throws func allOpenZircles() throws -> [ZircleEntity]? @@ -70,6 +70,7 @@ struct ConcreteZircle: ZircleEntity { } import MnemonicSwift + extension CombineSynchronizer: ZircleService { func closeZircle(name: String) throws { @@ -95,7 +96,7 @@ extension CombineSynchronizer: ZircleService { } } - func createNewZircle(name: String, goal zatoshi: Int64, frequency: ZircleFrequency, endDate: ZircleEndDate) -> Future { + func createNewZircle(name: String, goal zatoshi: Int64, frequency: ZircleFrequency, endDate: ZircleEndDate, spendingKey: String) -> Future { Future() { promise in var storage = [AnyCancellable]() @@ -105,7 +106,7 @@ extension CombineSynchronizer: ZircleService { // Get latest height from chain and generate a mnemonic seed for this zircle Publishers.Zip(self.latestHeight(), - Mnemonic.generatePublisher(strength: 24) + Mnemonic.generatePublisher(strength: 256) ).sink(receiveCompletion: { error in switch error { case .failure(let e): @@ -127,6 +128,7 @@ extension CombineSynchronizer: ZircleService { let extendedSpendingKey = extendedSpendingKeys.first, let extendedViewingKey = try derivationHelper.deriveExtendedFullViewingKey(extendedSpendingKey) else { promise(.failure(ZircleServiceError.generalError(message: "Key derivation error"))) + return } @@ -162,10 +164,7 @@ extension CombineSynchronizer: ZircleService { // get supporting wallet spending keys to create zircle - guard let mainSpendingKey = SeedManager.default.getKeys()?.first else { - promise(.failure(ZircleServiceError.generalError(message: "error getting spending keys"))) - return - } + guard let freq = CreateZircleMessage.ContributionFrequency(rawValue: zircle.frequency) else { promise(.failure(ZircleServiceError.generalError(message: "could not create frequency with value \(zircle.frequency)"))) @@ -194,7 +193,7 @@ extension CombineSynchronizer: ZircleService { height: height, spendingKey: extendedSpendingKey) // fund zircle - self.send(with: mainSpendingKey, + self.send(with: spendingKey, zatoshi: 1000, to: zAddr, memo: memo, @@ -226,11 +225,7 @@ extension CombineSynchronizer: ZircleService { func openInvite(_ url: URL) throws { - } - - - - + } } extension Mnemonic { @@ -239,7 +234,7 @@ extension Mnemonic { Future() { promise in DispatchQueue.global().async { - guard let mnemonic = Mnemonic.generateMnemonic(strength: 24) else { + guard let mnemonic = Mnemonic.generateMnemonic(strength: strength) else { promise(.failure(ZircleServiceError.generalError(message: "Error generating mnemonic"))) return } diff --git a/ZirclesTests/ZirclesTests.swift b/ZirclesTests/ZirclesTests.swift index 3a7d84c..84ba54a 100644 --- a/ZirclesTests/ZirclesTests.swift +++ b/ZirclesTests/ZirclesTests.swift @@ -8,27 +8,151 @@ import XCTest @testable import Zircles - +@testable import ZcashLightClientKit +import Combine class ZirclesTests: 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 cancellables = [AnyCancellable]() override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + SeedManager.default.nukeWallet() + coordinator = try TestCoordinator( + seed: seedPhrase, + walletBirthday: birthday, + channelProvider: ChannelProvider() + ) + try coordinator.reset(saplingActivation: 663150) } - + override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. + 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) + SeedManager.default.nukeWallet() } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + + func testNewCreateZircle() throws { + try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service) + let receivedTxHeight: BlockHeight = 663188 + + /* + 2. applyStaged(received_Tx_height) + */ + try coordinator.applyStaged(blockheight: receivedTxHeight) + + sleep(2) + let preTxExpectation = XCTestExpectation(description: "pre receive") + + /* + 3. sync up to received_Tx_height + */ + try coordinator.sync(completion: { (synchronizer) in + preTxExpectation.fulfill() + }, error: self.handleError) + + wait(for: [preTxExpectation], timeout: 5) + + let sendExpectation = XCTestExpectation(description: "sendToAddress") + let endDate = Calendar.current.date(byAdding: .day, value: 7, to: Date())! + + var incomingZircle: ZircleEntity? = nil + + coordinator.combineSynchronizer + .createNewZircle(name: "hackathon drinks", + goal: 3000000, + frequency: ZircleFrequency.daily, + endDate: ZircleEndDate.onDate(date: endDate), + spendingKey: coordinator.spendingKeys!.first! + ).receive(on: DispatchQueue.main) + .sink { (errorCompletion) in + switch errorCompletion { + case .failure(let error): + XCTFail("Test Failed - \(error)") + default: + break + } + + } receiveValue: { (zircle) in + incomingZircle = zircle + sendExpectation.fulfill() + } + .store(in: &cancellables) + wait(for: [sendExpectation], timeout: 60) + try coordinator.stageBlockCreate(height: receivedTxHeight + 1, count: 20) + + guard var receivedTx = try coordinator.getIncomingTransactions()?.first else { + XCTFail("did not receive back previously sent transaction") + return } + + + let zircleTxHeight = 663190 + + receivedTx.height = UInt64(zircleTxHeight) + + try coordinator.stageTransaction(receivedTx, at: zircleTxHeight) + + try coordinator.applyStaged(blockheight: zircleTxHeight) + + sleep(3) + + let postSendExpectation = XCTestExpectation(description: "post send expectation") + try coordinator.sync(completion: { (_) in + postSendExpectation.fulfill() + }, error: self.handleError) + + wait(for: [postSendExpectation], timeout: 5) + + guard let sentNewZircleTx = coordinator.synchronizer.sentTransactions.filter({ (sentTx) -> Bool in + sentTx.minedHeight == zircleTxHeight + }).first else { + XCTFail("Could not find Create New Zircle Transaction at height \(zircleTxHeight)") + return + } + + // let's try the memo thing out + guard let memoData = sentNewZircleTx.memo else { + XCTFail("retrieved transaction has no memo, when it should have") + return + } + + guard let memoString = memoData.asZcashTransactionMemo() else { + XCTFail("retrieved transaction has a memo that can't be converted to string") + return + } + var memoMessage: CreateZircleMessage! = nil + do { + memoMessage = try CreateZircleMessage(jsonString: memoString) + } catch { + XCTFail("failed to parse create new circle message") + return + } + XCTAssertEqual(memoMessage.frequency.rawValue, incomingZircle?.frequency) + XCTAssertEqual(memoMessage.name, incomingZircle?.name) +// XCTAssertEqual(memoMessage.goal, incomingZircle?.goal) + + } + + func handleError(_ error: Error?) { + _ = try? coordinator.stop() + guard let testError = error else { + XCTFail("failed with nil error") + return + } + XCTFail("Failed with error: \(testError)") } - } diff --git a/ZirclesTests/proto/darkside.grpc.swift b/ZirclesTests/proto/darkside.grpc.swift new file mode 100644 index 0000000..428b25d --- /dev/null +++ b/ZirclesTests/proto/darkside.grpc.swift @@ -0,0 +1,217 @@ +// +// DO NOT EDIT. +// +// Generated by the protocol buffer compiler. +// Source: darkside.proto +// + +// +// Copyright 2018, gRPC Authors All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import Foundation +import GRPC +import NIO +import NIOHTTP1 +import SwiftProtobuf +@testable import ZcashLightClientKit + + +/// Usage: instantiate DarksideStreamerClient, then call methods of this protocol to make API calls. +internal protocol DarksideStreamerClientProtocol { + func reset(_ request: DarksideMetaState, callOptions: CallOptions?) -> UnaryCall + func stageBlocksStream(callOptions: CallOptions?) -> ClientStreamingCall + func stageBlocks(_ request: DarksideBlocksURL, callOptions: CallOptions?) -> UnaryCall + func stageBlocksCreate(_ request: DarksideEmptyBlocks, callOptions: CallOptions?) -> UnaryCall + func stageTransactionsStream(callOptions: CallOptions?) -> ClientStreamingCall + func stageTransactions(_ request: DarksideTransactionsURL, callOptions: CallOptions?) -> UnaryCall + func applyStaged(_ request: DarksideHeight, callOptions: CallOptions?) -> UnaryCall + func getIncomingTransactions(_ request: Empty, callOptions: CallOptions?, handler: @escaping (RawTransaction) -> Void) -> ServerStreamingCall + func clearIncomingTransactions(_ request: Empty, callOptions: CallOptions?) -> UnaryCall +} + +internal final class DarksideStreamerClient: GRPCClient, DarksideStreamerClientProtocol { + internal let channel: GRPCChannel + internal var defaultCallOptions: CallOptions + + /// Creates a client for the cash.z.wallet.sdk.rpc.DarksideStreamer service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + internal init(channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions()) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + } + + /// Reset reverts all darksidewalletd state (active block range, latest height, + /// staged blocks and transactions) and lightwalletd state (cache) to empty, + /// the same as the initial state. This occurs synchronously and instantaneously; + /// no reorg happens in lightwalletd. This is good to do before each independent + /// test so that no state leaks from one test to another. + /// Also sets (some of) the values returned by GetLightdInfo(). + /// + /// - Parameters: + /// - request: Request to send to Reset. + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + internal func reset(_ request: DarksideMetaState, callOptions: CallOptions? = nil) -> UnaryCall { + return self.makeUnaryCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/Reset", + request: request, + callOptions: callOptions ?? self.defaultCallOptions) + } + + /// StageBlocksStream accepts a list of blocks and saves them into the blocks + /// staging area until ApplyStaged() is called; there is no immediate effect on + /// the mock zcashd. Blocks are hex-encoded. + /// + /// Callers should use the `send` method on the returned object to send messages + /// to the server. The caller should send an `.end` after the final message has been sent. + /// + /// - Parameters: + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. + internal func stageBlocksStream(callOptions: CallOptions? = nil) -> ClientStreamingCall { + return self.makeClientStreamingCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/StageBlocksStream", + callOptions: callOptions ?? self.defaultCallOptions) + } + + /// StageBlocks is the same as StageBlocksStream() except the blocks are fetched + /// from the given URL. Blocks are one per line, hex-encoded (not JSON). + /// + /// - Parameters: + /// - request: Request to send to StageBlocks. + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + internal func stageBlocks(_ request: DarksideBlocksURL, callOptions: CallOptions? = nil) -> UnaryCall { + return self.makeUnaryCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/StageBlocks", + request: request, + callOptions: callOptions ?? self.defaultCallOptions) + } + + /// StageBlocksCreate is like the previous two, except it creates 'count' + /// empty blocks at consecutive heights starting at height 'height'. The + /// 'nonce' is part of the header, so it contributes to the block hash; this + /// lets you create two fake blocks with the same transactions (or no + /// transactions) and same height, with two different hashes. + /// + /// - Parameters: + /// - request: Request to send to StageBlocksCreate. + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + internal func stageBlocksCreate(_ request: DarksideEmptyBlocks, callOptions: CallOptions? = nil) -> UnaryCall { + return self.makeUnaryCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/StageBlocksCreate", + request: request, + callOptions: callOptions ?? self.defaultCallOptions) + } + + /// StageTransactions stores the given transaction-height pairs in the + /// staging area until ApplyStaged() is called. Note that these transactions + /// are not returned by the production GetTransaction() gRPC until they + /// appear in a "mined" block (contained in the active blockchain presented + /// by the mock zcashd). + /// + /// Callers should use the `send` method on the returned object to send messages + /// to the server. The caller should send an `.end` after the final message has been sent. + /// + /// - Parameters: + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. + internal func stageTransactionsStream(callOptions: CallOptions? = nil) -> ClientStreamingCall { + return self.makeClientStreamingCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/StageTransactionsStream", + callOptions: callOptions ?? self.defaultCallOptions) + } + + /// Unary call to StageTransactions + /// + /// - Parameters: + /// - request: Request to send to StageTransactions. + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + internal func stageTransactions(_ request: DarksideTransactionsURL, callOptions: CallOptions? = nil) -> UnaryCall { + return self.makeUnaryCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/StageTransactions", + request: request, + callOptions: callOptions ?? self.defaultCallOptions) + } + + /// ApplyStaged iterates the list of blocks that were staged by the + /// StageBlocks*() gRPCs, in the order they were staged, and "merges" each + /// into the active, working blocks list that the mock zcashd is presenting + /// to lightwalletd. The resulting working block list can't have gaps; if the + /// working block range is 1000-1006, and the staged block range is 1003-1004, + /// the resulting range is 1000-1004, with 1000-1002 unchanged, blocks + /// 1003-1004 from the new range, and 1005-1006 dropped. After merging all + /// blocks, ApplyStaged() appends staged transactions (in the order received) + /// into each one's corresponding block. The staging area is then cleared. + /// + /// The argument specifies the latest block height that mock zcashd reports + /// (i.e. what's returned by GetLatestBlock). Note that ApplyStaged() can + /// also be used to simply advance the latest block height presented by mock + /// zcashd. That is, there doesn't need to be anything in the staging area. + /// + /// - Parameters: + /// - request: Request to send to ApplyStaged. + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + internal func applyStaged(_ request: DarksideHeight, callOptions: CallOptions? = nil) -> UnaryCall { + return self.makeUnaryCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/ApplyStaged", + request: request, + callOptions: callOptions ?? self.defaultCallOptions) + } + + /// Calls to the production gRPC SendTransaction() store the transaction in + /// a separate area (not the staging area); this method returns all transactions + /// in this separate area, which is then cleared. The height returned + /// with each transaction is -1 (invalid) since these transactions haven't + /// been mined yet. The intention is that the transactions returned here can + /// then, for example, be given to StageTransactions() to get them "mined" + /// into a specified block on the next ApplyStaged(). + /// + /// - Parameters: + /// - request: Request to send to GetIncomingTransactions. + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. + internal func getIncomingTransactions(_ request: Empty, callOptions: CallOptions? = nil, handler: @escaping (RawTransaction) -> Void) -> ServerStreamingCall { + return self.makeServerStreamingCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/GetIncomingTransactions", + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + handler: handler) + } + + /// Clear the incoming transaction pool. + /// + /// - Parameters: + /// - request: Request to send to ClearIncomingTransactions. + /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + internal func clearIncomingTransactions(_ request: Empty, callOptions: CallOptions? = nil) -> UnaryCall { + return self.makeUnaryCall(path: "/cash.z.wallet.sdk.rpc.DarksideStreamer/ClearIncomingTransactions", + request: request, + callOptions: callOptions ?? self.defaultCallOptions) + } + +} + + +// Provides conformance to `GRPCPayload` for request and response messages +extension DarksideMetaState: GRPCProtobufPayload {} +//extension Empty: GRPCProtobufPayload {} +extension DarksideBlock: GRPCProtobufPayload {} +extension DarksideBlocksURL: GRPCProtobufPayload {} +extension DarksideEmptyBlocks: GRPCProtobufPayload {} +//extension RawTransaction: GRPCProtobufPayload {} +extension DarksideTransactionsURL: GRPCProtobufPayload {} +extension DarksideHeight: GRPCProtobufPayload {} + diff --git a/ZirclesTests/proto/darkside.pb.swift b/ZirclesTests/proto/darkside.pb.swift new file mode 100644 index 0000000..0bc1226 --- /dev/null +++ b/ZirclesTests/proto/darkside.pb.swift @@ -0,0 +1,320 @@ +// DO NOT EDIT. +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: darkside.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright (c) 2019-2020 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct DarksideMetaState { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var saplingActivation: Int32 = 0 + + var branchID: String = String() + + var chainName: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A block is a hex-encoded string. +struct DarksideBlock { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var block: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// DarksideBlocksURL is typically something like: +/// https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt +struct DarksideBlocksURL { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var url: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// DarksideTransactionsURL refers to an HTTP source that contains a list +/// of hex-encoded transactions, one per line, that are to be associated +/// with the given height (fake-mined into the block at that height) +struct DarksideTransactionsURL { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var height: Int32 = 0 + + var url: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct DarksideHeight { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var height: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct DarksideEmptyBlocks { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var height: Int32 = 0 + + var nonce: Int32 = 0 + + var count: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "cash.z.wallet.sdk.rpc" + +extension DarksideMetaState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DarksideMetaState" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "saplingActivation"), + 2: .same(proto: "branchID"), + 3: .same(proto: "chainName"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularInt32Field(value: &self.saplingActivation) + case 2: try decoder.decodeSingularStringField(value: &self.branchID) + case 3: try decoder.decodeSingularStringField(value: &self.chainName) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.saplingActivation != 0 { + try visitor.visitSingularInt32Field(value: self.saplingActivation, fieldNumber: 1) + } + if !self.branchID.isEmpty { + try visitor.visitSingularStringField(value: self.branchID, fieldNumber: 2) + } + if !self.chainName.isEmpty { + try visitor.visitSingularStringField(value: self.chainName, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: DarksideMetaState, rhs: DarksideMetaState) -> Bool { + if lhs.saplingActivation != rhs.saplingActivation {return false} + if lhs.branchID != rhs.branchID {return false} + if lhs.chainName != rhs.chainName {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension DarksideBlock: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DarksideBlock" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "block"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self.block) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.block.isEmpty { + try visitor.visitSingularStringField(value: self.block, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: DarksideBlock, rhs: DarksideBlock) -> Bool { + if lhs.block != rhs.block {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension DarksideBlocksURL: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DarksideBlocksURL" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "url"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self.url) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.url.isEmpty { + try visitor.visitSingularStringField(value: self.url, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: DarksideBlocksURL, rhs: DarksideBlocksURL) -> Bool { + if lhs.url != rhs.url {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension DarksideTransactionsURL: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DarksideTransactionsURL" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "height"), + 2: .same(proto: "url"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularInt32Field(value: &self.height) + case 2: try decoder.decodeSingularStringField(value: &self.url) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.height != 0 { + try visitor.visitSingularInt32Field(value: self.height, fieldNumber: 1) + } + if !self.url.isEmpty { + try visitor.visitSingularStringField(value: self.url, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: DarksideTransactionsURL, rhs: DarksideTransactionsURL) -> Bool { + if lhs.height != rhs.height {return false} + if lhs.url != rhs.url {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension DarksideHeight: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DarksideHeight" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "height"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularInt32Field(value: &self.height) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.height != 0 { + try visitor.visitSingularInt32Field(value: self.height, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: DarksideHeight, rhs: DarksideHeight) -> Bool { + if lhs.height != rhs.height {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension DarksideEmptyBlocks: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DarksideEmptyBlocks" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "height"), + 2: .same(proto: "nonce"), + 3: .same(proto: "count"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularInt32Field(value: &self.height) + case 2: try decoder.decodeSingularInt32Field(value: &self.nonce) + case 3: try decoder.decodeSingularInt32Field(value: &self.count) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.height != 0 { + try visitor.visitSingularInt32Field(value: self.height, fieldNumber: 1) + } + if self.nonce != 0 { + try visitor.visitSingularInt32Field(value: self.nonce, fieldNumber: 2) + } + if self.count != 0 { + try visitor.visitSingularInt32Field(value: self.count, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: DarksideEmptyBlocks, rhs: DarksideEmptyBlocks) -> Bool { + if lhs.height != rhs.height {return false} + if lhs.nonce != rhs.nonce {return false} + if lhs.count != rhs.count {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/ZirclesTests/proto/darkside.proto b/ZirclesTests/proto/darkside.proto new file mode 100644 index 0000000..8ba56ed --- /dev/null +++ b/ZirclesTests/proto/darkside.proto @@ -0,0 +1,112 @@ +// Copyright (c) 2019-2020 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +syntax = "proto3"; +package cash.z.wallet.sdk.rpc; +option go_package = ".;walletrpc"; +option swift_prefix = ""; +import "service.proto"; + +message DarksideMetaState { + int32 saplingActivation = 1; + string branchID = 2; + string chainName = 3; +} + +// A block is a hex-encoded string. +message DarksideBlock { + string block = 1; +} + +// DarksideBlocksURL is typically something like: +// https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt +message DarksideBlocksURL { + string url = 1; +} + +// DarksideTransactionsURL refers to an HTTP source that contains a list +// of hex-encoded transactions, one per line, that are to be associated +// with the given height (fake-mined into the block at that height) +message DarksideTransactionsURL { + int32 height = 1; + string url = 2; +} + +message DarksideHeight { + int32 height = 1; +} + +message DarksideEmptyBlocks { + int32 height = 1; + int32 nonce = 2; + int32 count = 3; +} + +// Darksidewalletd maintains two staging areas, blocks and transactions. The +// Stage*() gRPCs add items to the staging area; ApplyStaged() "applies" everything +// in the staging area to the working (operational) state that the mock zcashd +// serves; transactions are placed into their corresponding blocks (by height). +service DarksideStreamer { + // Reset reverts all darksidewalletd state (active block range, latest height, + // staged blocks and transactions) and lightwalletd state (cache) to empty, + // the same as the initial state. This occurs synchronously and instantaneously; + // no reorg happens in lightwalletd. This is good to do before each independent + // test so that no state leaks from one test to another. + // Also sets (some of) the values returned by GetLightdInfo(). + rpc Reset(DarksideMetaState) returns (Empty) {} + + // StageBlocksStream accepts a list of blocks and saves them into the blocks + // staging area until ApplyStaged() is called; there is no immediate effect on + // the mock zcashd. Blocks are hex-encoded. + rpc StageBlocksStream(stream DarksideBlock) returns (Empty) {} + + // StageBlocks is the same as StageBlocksStream() except the blocks are fetched + // from the given URL. Blocks are one per line, hex-encoded (not JSON). + rpc StageBlocks(DarksideBlocksURL) returns (Empty) {} + + // StageBlocksCreate is like the previous two, except it creates 'count' + // empty blocks at consecutive heights starting at height 'height'. The + // 'nonce' is part of the header, so it contributes to the block hash; this + // lets you create two fake blocks with the same transactions (or no + // transactions) and same height, with two different hashes. + rpc StageBlocksCreate(DarksideEmptyBlocks) returns (Empty) {} + + // StageTransactions stores the given transaction-height pairs in the + // staging area until ApplyStaged() is called. Note that these transactions + // are not returned by the production GetTransaction() gRPC until they + // appear in a "mined" block (contained in the active blockchain presented + // by the mock zcashd). + rpc StageTransactionsStream(stream RawTransaction) returns (Empty) {} + + rpc StageTransactions(DarksideTransactionsURL) returns (Empty) {} + + + // ApplyStaged iterates the list of blocks that were staged by the + // StageBlocks*() gRPCs, in the order they were staged, and "merges" each + // into the active, working blocks list that the mock zcashd is presenting + // to lightwalletd. The resulting working block list can't have gaps; if the + // working block range is 1000-1006, and the staged block range is 1003-1004, + // the resulting range is 1000-1004, with 1000-1002 unchanged, blocks + // 1003-1004 from the new range, and 1005-1006 dropped. After merging all + // blocks, ApplyStaged() appends staged transactions (in the order received) + // into each one's corresponding block. The staging area is then cleared. + // + // The argument specifies the latest block height that mock zcashd reports + // (i.e. what's returned by GetLatestBlock). Note that ApplyStaged() can + // also be used to simply advance the latest block height presented by mock + // zcashd. That is, there doesn't need to be anything in the staging area. + rpc ApplyStaged(DarksideHeight) returns (Empty) {} + + // Calls to the production gRPC SendTransaction() store the transaction in + // a separate area (not the staging area); this method returns all transactions + // in this separate area, which is then cleared. The height returned + // with each transaction is -1 (invalid) since these transactions haven't + // been mined yet. The intention is that the transactions returned here can + // then, for example, be given to StageTransactions() to get them "mined" + // into a specified block on the next ApplyStaged(). + rpc GetIncomingTransactions(Empty) returns (stream RawTransaction) {} + + // Clear the incoming transaction pool. + rpc ClearIncomingTransactions(Empty) returns (Empty) {} +} diff --git a/ZirclesTests/utils/DarkSideWalletService.swift b/ZirclesTests/utils/DarkSideWalletService.swift new file mode 100644 index 0000000..c7747fc --- /dev/null +++ b/ZirclesTests/utils/DarkSideWalletService.swift @@ -0,0 +1,151 @@ +// +// DarkSideWalletService.swift +// ZcashLightClientKit-Unit-Tests +// +// Created by Francisco Gindre on 3/23/20. +// + +import Foundation +@testable import ZcashLightClientKit +import GRPC + +enum DarksideDataset: String { + case afterLargeReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-large.txt" + case afterSmallReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt" + case beforeReOrg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt" + + /** + see + https://github.com/zcash-hackworks/darksidewalletd-test-data/tree/master/tx-index-reorg + */ + case txIndexChangeBefore = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/before-reorg.txt" + + case txIndexChangeAfter = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/after-reorg.txt" + + /** + See https://github.com/zcash-hackworks/darksidewalletd-test-data/tree/master/tx-height-reorg + */ + case txHeightReOrgBefore = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-height-reorg/before-reorg.txt" + + case txHeightReOrgAfter = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-height-reorg/after-reorg.txt" + + /* + see: https://github.com/zcash-hackworks/darksidewalletd-test-data/tree/master/tx-remove-reorg + */ + case txReOrgRemovesInboundTxBefore = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-remove-reorg/before-reorg.txt" + + case txReOrgRemovesInboundTxAfter = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-remove-reorg/after-reorg.txt" +} + +class DarksideWalletService: LightWalletService { + + func fetchTransaction(txId: Data) throws -> TransactionEntity { + try service.fetchTransaction(txId: txId) + } + + func fetchTransaction(txId: Data, result: @escaping (Result) -> Void) { + service.fetchTransaction(txId: txId, result: result) + } + + var channel: Channel + init(channelProvider: ChannelProvider) { + self.channel = ChannelProvider().channel() + self.service = LightWalletGRPCService(channel: channel) + self.darksideService = DarksideStreamerClient(channel: channel) + } + + convenience init() { + self.init(channelProvider: ChannelProvider()) + } + var service: LightWalletGRPCService + var darksideService: DarksideStreamerClient + + func latestBlockHeight(result: @escaping (Result) -> Void) { + service.latestBlockHeight(result: result) + } + + func latestBlockHeight() throws -> BlockHeight { + try service.latestBlockHeight() + } + + func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void) { + service.blockRange(range, result: result) + } + + func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] { + try service.blockRange(range) + } + + /** + Darskside lightwalletd should do a fake submission, by sending over the tx, retrieving it and including it in a new block + */ + func submit(spendTransaction: Data, result: @escaping (Result) -> Void) { + service.submit(spendTransaction: spendTransaction, result: result) + } + + func submit(spendTransaction: Data) throws -> LightWalletServiceResponse { + try service.submit(spendTransaction: spendTransaction) + } + + func useDataset(_ datasetUrl: String) throws { + try useDataset(from: datasetUrl) + } + + func useDataset(from urlString: String) throws { + var blocksUrl = DarksideBlocksURL() + blocksUrl.url = urlString + _ = try darksideService.stageBlocks(blocksUrl, callOptions: nil).response.wait() + } + + func applyStaged(nextLatestHeight: BlockHeight) throws { + var darksideHeight = DarksideHeight() + darksideHeight.height = Int32(nextLatestHeight) + _ = try darksideService.applyStaged(darksideHeight).response.wait() + } + + func clearIncomingTransactions() throws { + _ = try darksideService.clearIncomingTransactions(Empty()).response.wait() + } + + func getIncomingTransactions() throws -> [RawTransaction]? { + var txs = [RawTransaction]() + let response = try darksideService.getIncomingTransactions(Empty(), handler: { txs.append($0) }).status.wait() + switch response.code { + case .ok: + return txs.count > 0 ? txs : nil + default: + throw response + } + } + + func reset(saplingActivation: BlockHeight) throws { + var metaState = DarksideMetaState() + metaState.saplingActivation = Int32(saplingActivation) + metaState.branchID = "d3adb33f" + metaState.chainName = "test" + // TODO: complete meta state correctly + _ = try darksideService.reset(metaState).response.wait() + } + + func stageBlocksCreate(from height: BlockHeight, count: Int = 1, nonce: Int = 0) throws { + var emptyBlocks = DarksideEmptyBlocks() + emptyBlocks.count = Int32(count) + emptyBlocks.height = Int32(height) + emptyBlocks.nonce = Int32(nonce) + _ = try darksideService.stageBlocksCreate(emptyBlocks).response.wait() + } + + func stageTransaction(_ rawTransaction: RawTransaction, at height: BlockHeight) throws { + var tx = rawTransaction + tx.height = UInt64(height) + _ = try darksideService.stageTransactionsStream().sendMessage(tx).wait() + } + + func stageTransaction(from url: String, at height: BlockHeight) throws { + var txUrl = DarksideTransactionsURL() + txUrl.height = Int32(height) + txUrl.url = url + _ = try darksideService.stageTransactions(txUrl, callOptions: nil).response.wait() + } + +} diff --git a/ZirclesTests/utils/FakeChainBuilder.swift b/ZirclesTests/utils/FakeChainBuilder.swift new file mode 100644 index 0000000..18b2c12 --- /dev/null +++ b/ZirclesTests/utils/FakeChainBuilder.swift @@ -0,0 +1,116 @@ +// +// FakeChainBuilder.swift +// ZcashLightClientKit-Unit-Tests +// +// Created by Francisco Gindre on 5/21/20. +// + +import Foundation +@testable import ZcashLightClientKit + +enum FakeChainBuilderError: Error { + case fakeHexDataConversionFailed +} +class FakeChainBuilder { + static let someOtherTxUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt" + static let txMainnetBlockUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/663150.txt" + static func buildChain(darksideWallet: DarksideWalletService) throws { + try darksideWallet.reset(saplingActivation: 663150) + try darksideWallet.useDataset(from: txMainnetBlockUrl) + + try darksideWallet.stageBlocksCreate(from: 663151, count: 100) + + try darksideWallet.stageTransaction(from: txUrls[663174]!, at: 663174) + + try darksideWallet.stageTransaction(from: txUrls[663188]!, at: 663188) + + } + + static func buildTxUrl(for id: String) -> String { + "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/\(id).txt" + } + + static var txUrls = [ + 663174 : buildTxUrl(for: "8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a"), + 663188 : buildTxUrl(for: "15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590"), + 663202 : buildTxUrl(for: "d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c"), + 663218 : buildTxUrl(for: "e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce"), + 663229 : buildTxUrl(for: "0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f"), + 663849 : buildTxUrl(for: "c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495"), + 663891 : buildTxUrl(for: "72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d"), + 663922 : buildTxUrl(for: "ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01"), + 663938 : buildTxUrl(for: "34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08"), + 663942 : buildTxUrl(for: "6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c"), + 663947 : buildTxUrl(for: "de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0"), + 663949 : buildTxUrl(for: "4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b"), + 663953 : buildTxUrl(for: "e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8"), + 663956 : buildTxUrl(for: "73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc"), + 663974 : buildTxUrl(for: "4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7"), + 664003 : buildTxUrl(for: "d2e859e8ef8ab27355c7a6caf643065d2d7a720e334c4a84943f6d1ae3919b5d"), + 664012 : buildTxUrl(for: "547784f746eef2f164bbb1a56882723dde744157a21e4fdfeadee763f73fee84"), + 664022 : buildTxUrl(for: "981638bb7ac08e31ee6db5c70d98ad6b137a448716b19245f9454b450c07c911"), + 664037 : buildTxUrl(for: "36505ab3c78c62981c8111d143cd57dcfe6cafcb2c3cdc258b023ae5210d53f1"), + 664038 : buildTxUrl(for: "0ffc55af750bb10a9e6a7e425138cc5acb5f7ddca68bf9d0c4606437bd692622"), + 678828 : buildTxUrl(for: "cfd3bce9fdeeae12b99fdb977a997177e183c2312871f0454bdf61640cc03d93"), + 678836 : buildTxUrl(for: "5af3bc9818e5fabcc691f319d7354cc4194f17727f6303d59a94c3e5f0daf560"), + 682588 : buildTxUrl(for: "b1f566dec94048ff81306884b6ed92eb73cdb768b738d9c8cbd94babc1f0a9c9"), + 683689 : buildTxUrl(for: "3b568a1547832ac28bfcaf4c269f85fd68083735790f7949aa3a548ab53acf65"), + 683791 : buildTxUrl(for: "9e2eb538207ab47356a3723fd0e6f44b9349ea944d9c2d7be0d4e3a6a02c2c29"), + 683795 : buildTxUrl(for: "15d2f32494271f0a60f3928e4fc79c2cea337e06fbbbe7f6fb4a0d36002a0d42"), + 683809 : buildTxUrl(for: "76be7c244c37e1710bbb9f162baab265eebc8a379ad1843435ba5e7a2c21a600"), + 684374 : buildTxUrl(for: "3640a35c02cf4d9e0fa178380173b193873d8a0ef4bad57dd43e7d95db450c89"), + 685126 : buildTxUrl(for: "86f3457bdb8793a413c009a8a7e128b5a82723f41ebe557327bbe555fd47fbf3"), + 687746 : buildTxUrl(for: "edb32a55d5fa18fc5c6bf09f5f1de198b219b6780ca71bbc4fd321b655bbfe42"), + 687900 : buildTxUrl(for: "855af341c14b94fec67e5eb56bb801a59551df33e9d955982672f5f62e76f72e"), + 688059 : buildTxUrl(for: "57c226f77ad01ecf833515612e7cba7abe64500fa891144c2c89c59af8c36c22"), + 691540 : buildTxUrl(for: "9a74cd7f170f6c8cef04f3327fdcf63ec69dd1263f80c9bf0b3002c871950ddd"), + 691593 : buildTxUrl(for: "6b64134034ec282092501f85bf8955006894dbcac402fa5e6c85ee867334cd3d"), + 691632 : buildTxUrl(for: "75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2"), + 692981 : buildTxUrl(for: "f98f2c75785f110203930c7fd4115019ec70af6470db1be052985b469906fe98"), + 692984 : buildTxUrl(for: "67138ad7e5e97216124c2bbcda8edb7687c2cfbf5d644df2af2a86344437a661"), + 692992 : buildTxUrl(for: "6cf507ab4d3255fa51679c0256a1be1d668786bd3f558000f9e90ec442514212"), + 693248 : buildTxUrl(for: "d1278d74424807b830256ccbd4d7624dc9e68a50760f870a55c8e99715072ef1"), + 693268 : buildTxUrl(for: "e56c84718de5dee049b31c89832f4bf1694268e2664a04df182a8797cb00b52e"), + 693325 : buildTxUrl(for: "5635f48dc99adfebb0be105231b9383bd2d0df64e43a780d11620390640b8d3d"), + 693398 : buildTxUrl(for: "26c41d5dbbaaa3934b37109645b0aece9600248c5f51404d1f4ea7b711ac3312"), + 693460 : buildTxUrl(for: "8d381a3d993c8d424db0907bab3fef6000bc8de9efe7186846d44dd6d6a014b1"), + 693475 : buildTxUrl(for: "900f2a406c1546126e1dba0e4e6ca0e092ebe697a2f7b0abee4e9771e1038f0b"), + 693718 : buildTxUrl(for: "7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6"), + 693736 : buildTxUrl(for: "34a4d630f120e4c1e7d2b9844c69fd4d3be71532ade1aaf7147566f05162c316"), + 696005 : buildTxUrl(for: "076d30ca62082dda9a760e0d004393cd96830056c6dca643fccdbe500053e355"), + 696012 : buildTxUrl(for: "e2da49325057b2232e85b0228955234f4a3538df2ebf4cd121589bac9771f6f2"), + 696040 : buildTxUrl(for: "4f6ef63bd3be8338c902901daf77ab5aa23dd97c160ee91b00950accf7f0b194"), + 698361 : buildTxUrl(for: "d275a9e96e6c68dfb8fe6ec3fd39737ce5fa880f86552b3ed993048373d6e8ad"), + 710823 : buildTxUrl(for: "bac04ad7734628e70a57408c65403ec845bce575197e7984435976e1ac64ae4f"), + 710896 : buildTxUrl(for: "56c63ef496f633418f0576cc34a0730c74023d78003b95aff731e0448c8b9203"), + 711847 : buildTxUrl(for: "82439eade5d1deba7606f3db53bf33588677b1bd9765a5eb5f4d3f6980ecb3d4"), + 727486 : buildTxUrl(for: "b5877c7f7dd3856bae679f7ccb37ddf3fcd2fafe72a081878ee9069fc25934cd"), + 728159 : buildTxUrl(for: "5d6a0c4879a244d2c0a6e2d26c4d0d26dee5a5c1f3f13f42436253272d4b8a03"), + 736102 : buildTxUrl(for: "be3a3a3fe10b9a1976410e5aaf425b24695dcdd04df926a23d9f3f8ed43178c6"), + 736254 : buildTxUrl(for: "acc0685aee04f7b7c6a12c969c1646038ea4a3b940d00b28d1eaf7643602d49e"), + 736262 : buildTxUrl(for: "fed00ff5cb6ee057d00ec70f1f5f1b189d591903c1e1cbde654ad39c8477808c"), + 736301 : buildTxUrl(for: "f3ef9f3adedf2b66e438c9d7d878ed72886b62b70e68547bb47d5b6033519dcd"), + 736574 : buildTxUrl(for: "34574442629a2378eccd216385d8bc99859e214e79265941319599130de2c69a"), + 739582 : buildTxUrl(for: "71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf"), + 741148 : buildTxUrl(for: "5eff7f15b39b9ab463767b768e23f90b4a23239ed873fdfbd4afa286027f7b57"), + 741154 : buildTxUrl(for: "b05c3df882ccff4f58acc1e3dbe2520213159d584bac01ff0199c37c25451430"), + 741156 : buildTxUrl(for: "1a3bb3d4fece0fcde1a47ef8271511cefcdb67f2698afc2c63297fbeab2003d8"), + 741158 : buildTxUrl(for: "a979dc83f55d9114dcab2eb5694bbf4fbb84602ceb27af6e287d6af8775d92c7"), + 741162 : buildTxUrl(for: "23278a3c1bf03f20f67299ed0b8dc4d577909d2344f1f02971c8890c6341d79d"), + 741170 : buildTxUrl(for: "db4101f3cccb1671dc1557670fa8b4e64c958008778b8ab1779a4a2969fe1153"), + 741171 : buildTxUrl(for: "74a94aceedb3a22eedb0b5d450487340b3783e1d22ef47af2359c45d0804d9ff"), + 741172 : buildTxUrl(for: "2899ccaea26e4c873a09965e0c268c96a86b1931d896b8622f36422d32c234c2"), + 741174 : buildTxUrl(for: "819009ec1d0cfb50d30c944a41bde545ee631663af39f8a17c31255ada12de13"), + 775018 : buildTxUrl(for: "85b3b64903b1873f5b7578eb2f167752b6a66ba64bb5c4cb8a4d75072219678b"), + 775021 : buildTxUrl(for: "6d69d23c8db7736efdd38090c3cd032f8e68431272964157c52a924315e1a3f5"), + 775267 : buildTxUrl(for: "daf24871749c8360028a19e4d82ddb0d573d7c765a894d601aa241f1e040ac5f"), + 776019 : buildTxUrl(for: "f64378feb08c30b28a90f31e8cd84a932ed064108fb17a3e0aee1585ff994138"), + 776158 : buildTxUrl(for: "9339a0a231f88b3067f3378c7ae70170fdf4246e0e70f442552a6e3961391b56"), + 776233 : buildTxUrl(for: "c9c33e44468c1fa0ee5f9d411b43748f8882915640b3b13c6e48c56e9cdde798"), + 776240 : buildTxUrl(for: "0e1c70fc67d3b9ae29a98996d4363b512d51d7b8422a6fa58f5803bebb247e7a"), + 820691 : buildTxUrl(for: "1948bc40226e53d2652f593ebe4f34c5d81550eeb16fe2ed797b7ef3c1083899"), + 822410 : buildTxUrl(for: "f3f8684be8d77367d099a38f30e3652410cdebe35c006d0599d86d8ec640867f"), + 828933 : buildTxUrl(for: "1fd394257d1c10c8a70fb760cf73f6d0e96e61edcf1ffca6da12d733a59221a4") + ] + +} diff --git a/ZirclesTests/utils/FakeService.swift b/ZirclesTests/utils/FakeService.swift new file mode 100644 index 0000000..929f153 --- /dev/null +++ b/ZirclesTests/utils/FakeService.swift @@ -0,0 +1,65 @@ +// +// FakeService.swift +// ZcashLightClientKitTests +// +// Created by Francisco Gindre on 10/23/19. +// Copyright © 2019 Electric Coin Company. All rights reserved. +// + +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()) + var latestHeight: BlockHeight + + init(latestBlockHeight: BlockHeight) { + self.latestHeight = latestBlockHeight + } + func latestBlockHeight(result: @escaping (Result) -> Void) { + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + result(.success(self.latestHeight)) + } + } + + func latestBlockHeight() throws -> BlockHeight { + return self.latestHeight + } + + func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void) { + self.service.blockRange(range, result: result) + } + + func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] { + try self.service.blockRange(range) + } + + func submit(spendTransaction: Data, result: @escaping (Result) -> 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()) + } + + func fetchTransaction(txId: Data) throws -> TransactionEntity { + Transaction(id: 1, transactionId: Data(), created: "Today", transactionIndex: 1, expiryHeight: -1, minedHeight: -1, raw: nil) + } + + func fetchTransaction(txId: Data, result: @escaping (Result) -> Void) { + + } + +} diff --git a/ZirclesTests/utils/FakeStorage.swift b/ZirclesTests/utils/FakeStorage.swift new file mode 100644 index 0000000..e021bf8 --- /dev/null +++ b/ZirclesTests/utils/FakeStorage.swift @@ -0,0 +1,64 @@ +// +// FakeStorage.swift +// ZcashLightClientKit +// +// Created by Francisco Gindre on 12/09/2019. +// Copyright © 2019 Electric Coin Company. All rights reserved. +// + +import Foundation +@testable import ZcashLightClientKit + +class ZcashConsoleFakeStorage: CompactBlockRepository { + func latestHeight() throws -> Int { + return self.latestBlockHeight + } + + func write(blocks: [ZcashCompactBlock]) throws { + fakeSave(blocks: blocks) + } + + func rewind(to height: BlockHeight) throws { + fakeRewind(to: height) + } + + var latestBlockHeight: BlockHeight = 0 + var delay = DispatchTimeInterval.milliseconds(300) + + init(latestBlockHeight: BlockHeight = 0) { + self.latestBlockHeight = latestBlockHeight + } + + func latestHeight(result: @escaping (Result) -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + result(.success(self.latestBlockHeight)) + } + } + + fileprivate func fakeSave(blocks: [ZcashCompactBlock]) { + blocks.forEach { + LoggerProxy.debug("saving block \($0)") + self.latestBlockHeight = $0.height + } + } + + func write(blocks: [ZcashCompactBlock], completion: ((Error?) -> Void)?) { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.fakeSave(blocks: blocks) + completion?(nil) + } + } + + func rewind(to height: BlockHeight, completion: ((Error?) -> Void)?) { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.fakeRewind(to: height) + completion?(nil) + } + } + + private func fakeRewind(to height: BlockHeight) { + LoggerProxy.debug("rewind to \(height)") + self.latestBlockHeight = min(self.latestBlockHeight, height) + } + +} diff --git a/ZirclesTests/utils/MockTransactionRepository.swift b/ZirclesTests/utils/MockTransactionRepository.swift new file mode 100644 index 0000000..43d39d5 --- /dev/null +++ b/ZirclesTests/utils/MockTransactionRepository.swift @@ -0,0 +1,168 @@ +// +// MockTransactionRepository.swift +// ZcashLightClientKit-Unit-Tests +// +// Created by Francisco Gindre on 12/6/19. +// + +import Foundation + +@testable import ZcashLightClientKit + +class MockTransactionRepository: TransactionRepository { + func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? { + nil + } + +// func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? { +// nil +// } + + + var unminedCount: Int + var receivedCount: Int + var sentCount: Int + + var transactions: [ConfirmedTransactionEntity] = [] + var reference: [Kind] = [] + var sentTransactions: [ConfirmedTransaction] = [] + var receivedTransactions: [ConfirmedTransaction] = [] + + var allCount: Int { + receivedCount + sentCount + } + + init(unminedCount: Int, receivedCount: Int, sentCount: Int) { + self.unminedCount = unminedCount + self.receivedCount = receivedCount + self.sentCount = sentCount + } + + func generate() { + + var txArray = [ConfirmedTransactionEntity]() + reference = referenceArray() + for i in 0 ..< reference.count { + txArray.append(mockTx(index: i, kind: reference[i])) + } + transactions = txArray + } + + func countAll() throws -> Int { + allCount + } + + func countUnmined() throws -> Int { + unminedCount + } + + func findBy(id: Int) throws -> TransactionEntity? { + transactions.first(where: {$0.id == id})?.transactionEntity + } + + func findBy(rawId: Data) throws -> TransactionEntity? { + transactions.first(where: {$0.rawTransactionId == rawId})?.transactionEntity + } + + func findAllSentTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? { + guard let indices = reference.indices(where: { $0 == .sent }) else { return nil } + + let sentTxs = indices.map { (idx) -> ConfirmedTransactionEntity in + transactions[idx] + } + return slice(txs: sentTxs, offset: offset, limit: limit) + } + + + func findAllReceivedTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? { + guard let indices = reference.indices(where: { $0 == .received }) else { return nil } + + let receivedTxs = indices.map { (idx) -> ConfirmedTransactionEntity in + transactions[idx] + } + return slice(txs: receivedTxs, offset: offset, limit: limit) + } + + func findAll(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? { + transactions + } + + func lastScannedHeight() throws -> BlockHeight { + return 700000 + } + + func isInitialized() throws -> Bool { + true + } + + func findEncodedTransactionBy(txId: Int) -> EncodedTransaction? { + nil + } + + enum Kind { + case sent + case received + } + + func referenceArray() -> [Kind] { + var template = [Kind]() + + for _ in 0 ..< sentCount { + template.append(.sent) + } + for _ in 0 ..< receivedCount { + template.append(.received) + } + return template.shuffled() + } + + + func mockTx(index: Int, kind: Kind) -> ConfirmedTransactionEntity { + switch kind { + case .received: + return mockReceived(index) + case .sent: + return mockSent(index) + } + } + + func mockSent(_ index: Int) -> ConfirmedTransactionEntity { + ConfirmedTransaction(toAddress: "some_address", expiryHeight: BlockHeight.max, minedHeight: randomBlockHeight(), noteId: index, blockTimeInSeconds: randomTimeInterval(), transactionIndex: index, raw: Data(), id: index, value: Int.random(in: 1 ... ZcashSDK.ZATOSHI_PER_ZEC), memo: nil, rawTransactionId: Data()) + } + + + func mockReceived(_ index: Int) -> ConfirmedTransactionEntity { + ConfirmedTransaction(toAddress: nil, expiryHeight: BlockHeight.max, minedHeight: randomBlockHeight(), noteId: index, blockTimeInSeconds: randomTimeInterval(), transactionIndex: index, raw: Data(), id: index, value: Int.random(in: 1 ... ZcashSDK.ZATOSHI_PER_ZEC), memo: nil, rawTransactionId: Data()) + } + + func randomBlockHeight() -> BlockHeight { + BlockHeight.random(in: ZcashSDK.SAPLING_ACTIVATION_HEIGHT ... 1_000_000) + } + func randomTimeInterval() -> TimeInterval { + Double.random(in: Date().timeIntervalSince1970 - 1000000.0 ... Date().timeIntervalSince1970) + } + + func slice(txs: [ConfirmedTransactionEntity], offset: Int, limit: Int) -> [ConfirmedTransactionEntity] { + guard offset < txs.count else { return [] } + + return Array(txs[offset ..< min(offset + limit, txs.count - offset)]) + } +} + +extension MockTransactionRepository.Kind: Equatable {} + +extension Array { + func indices(where f: (_ element: Element) -> Bool) -> [Int]? { + guard self.count > 0 else { return nil } + var idx = [Int]() + for i in 0 ..< self.count { + if f(self[i]) { + idx.append(i) + } + } + + guard idx.count > 0 else { return nil } + return idx + + } +} diff --git a/ZirclesTests/utils/SampleLogger.swift b/ZirclesTests/utils/SampleLogger.swift new file mode 100644 index 0000000..eb41eb0 --- /dev/null +++ b/ZirclesTests/utils/SampleLogger.swift @@ -0,0 +1,61 @@ +// +// SampleLogger.swift +// ZcashLightClientSample +// +// Created by Francisco Gindre on 3/9/20. +// Copyright © 2020 Electric Coin Company. All rights reserved. +// + +import Foundation +import os +import ZcashLightClientKit +class SampleLogger: ZcashLightClientKit.Logger { + enum LogLevel: Int { + case debug + case error + case warning + case event + case info + } + + var level: LogLevel + init(logLevel: LogLevel) { + self.level = logLevel + } + + private static let subsystem = Bundle.main.bundleIdentifier! + static let oslog = OSLog(subsystem: subsystem, category: "test-logs") + + func debug(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue == LogLevel.debug.rawValue else { return } + log(level: "DEBUG 🐞", message: message, file: file, function: function, line: line) + } + + func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.error.rawValue else { return } + log(level: "ERROR 💥", message: message, file: file, function: function, line: line) + } + + func warn(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.warning.rawValue else { return } + log(level: "WARNING ⚠️", message: message, file: file, function: function, line: line) + } + + func event(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.event.rawValue else { return } + log(level: "EVENT ⏱", message: message, file: file, function: function, line: line) + } + + func info(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.info.rawValue else { return } + log(level: "INFO ℹ️", message: message, file: file, function: function, line: line) + } + + private func log(level: String, message: String, file: String, function: String, line: Int) { + let fileName = file as NSString + + os_log("[%@] %@ - %@ - Line: %d -> %@", log: Self.oslog, type: .default, level, fileName.lastPathComponent, function, line, message) + } + + +} diff --git a/ZirclesTests/utils/Stubs.swift b/ZirclesTests/utils/Stubs.swift new file mode 100644 index 0000000..74be522 --- /dev/null +++ b/ZirclesTests/utils/Stubs.swift @@ -0,0 +1,235 @@ +// +// Stubs.swift +// ZcashLightClientKitTests +// +// Created by Francisco Gindre on 18/09/2019. +// Copyright © 2019 Electric Coin Company. All rights reserved. +// + +import Foundation +import GRPC +import SwiftProtobuf +@testable import ZcashLightClientKit + +class AwfulLightWalletService: MockLightWalletService { + override func latestBlockHeight() throws -> BlockHeight { + throw LightWalletServiceError.generalError + } + + override func blockRange(_ range: CompactBlockRange) throws -> [ZcashCompactBlock] { + throw LightWalletServiceError.invalidBlock + } + + override func latestBlockHeight(result: @escaping (Result) -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + result(.failure(LightWalletServiceError.generalError)) + } + + } + + override func blockRange(_ range: CompactBlockRange, result: @escaping (Result<[ZcashCompactBlock], LightWalletServiceError>) -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + result(.failure(LightWalletServiceError.generalError)) + } + } + + override func submit(spendTransaction: Data, result: @escaping(Result) -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + result(.failure(LightWalletServiceError.generalError)) + } + } + + /** + Submits a raw transaction over lightwalletd. Blocking + */ + + override func submit(spendTransaction: Data) throws -> LightWalletServiceResponse { + throw LightWalletServiceError.generalError + } +} + +class SlightlyBadLightWalletService: MockLightWalletService { + + + override func submit(spendTransaction: Data, result: @escaping(Result) -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + result(.success(LightWalletServiceMockResponse.error)) + } + } + + /** + Submits a raw transaction over lightwalletd. Blocking + */ + + override func submit(spendTransaction: Data) throws -> LightWalletServiceResponse { + LightWalletServiceMockResponse.error + } +} + + +extension LightWalletServiceMockResponse { + static var error: LightWalletServiceMockResponse { + LightWalletServiceMockResponse(errorCode: -100, errorMessage: "Ohhh this is bad dude, really bad, you lost all your internet money", unknownFields: UnknownStorage()) + } + static var success: LightWalletServiceMockResponse { + LightWalletServiceMockResponse(errorCode: 0, errorMessage: "", unknownFields: UnknownStorage()) + } +} + +class MockRustBackend: ZcashRustBackendWelding { + + static var mockDataDb = false + static var mockAcounts = false + static var mockError: RustWeldingError? + static var mockLastError: String? + static var mockAccounts: [String]? + static var mockAddresses: [String]? + static var mockBalance: Int64? + static var mockVerifiedBalance: Int64? + static var mockMemo: String? + static var mockSentMemo: String? + static var mockValidateCombinedChainSuccessRate: Float? + static var mockValidateCombinedChainFailAfterAttempts: Int? + static var mockValidateCombinedChainKeepFailing = false + static var mockValidateCombinedChainFailureHeight: BlockHeight = 0 + static var mockScanblocksSuccessRate: Float? + static var mockCreateToAddress: Int64? + static var rustBackend = ZcashRustBackend.self + + + static func lastError() -> RustWeldingError? { + mockError ?? rustBackend.lastError() + } + + static func getLastError() -> String? { + mockLastError ?? rustBackend.getLastError() + } + + static func isValidShieldedAddress(_ address: String) throws -> Bool { + true + } + + static func isValidTransparentAddress(_ address: String) throws -> Bool { + true + } + + static func initDataDb(dbData: URL) throws { + if !mockDataDb { + try rustBackend.initDataDb(dbData: dbData) + } + } + + static func initAccountsTable(dbData: URL, seed: [UInt8], accounts: Int32) -> [String]? { + mockAccounts ?? rustBackend.initAccountsTable(dbData: dbData, seed: seed, accounts: accounts) + } + + static func initBlocksTable(dbData: URL, height: Int32, hash: String, time: UInt32, saplingTree: String) throws { + if !mockDataDb { + try rustBackend.initBlocksTable(dbData: dbData, height: height, hash: hash, time: time, saplingTree: saplingTree) + } + } + + static func getAddress(dbData: URL, account: Int32) -> String? { + mockAddresses?[Int(account)] ?? rustBackend.getAddress(dbData: dbData, account: account) + } + + static func getBalance(dbData: URL, account: Int32) -> Int64 { + mockBalance ?? rustBackend.getBalance(dbData: dbData, account: account) + } + + static func getVerifiedBalance(dbData: URL, account: Int32) -> Int64 { + mockVerifiedBalance ?? rustBackend.getVerifiedBalance(dbData: dbData, account: account) + } + + static func getReceivedMemoAsUTF8(dbData: URL, idNote: Int64) -> String? { + mockMemo ?? rustBackend.getReceivedMemoAsUTF8(dbData: dbData, idNote: idNote) + } + + static func getSentMemoAsUTF8(dbData: URL, idNote: Int64) -> String? { + mockSentMemo ?? getSentMemoAsUTF8(dbData: dbData, idNote: idNote) + } + + static func validateCombinedChain(dbCache: URL, dbData: URL) -> Int32 { + if let rate = self.mockValidateCombinedChainSuccessRate { + if shouldSucceed(successRate: rate) { + return validationResult(dbCache: dbCache, dbData: dbData) + } else { + return Int32(mockValidateCombinedChainFailureHeight) + } + } else if let attempts = self.mockValidateCombinedChainFailAfterAttempts { + self.mockValidateCombinedChainFailAfterAttempts = attempts - 1 + if attempts > 0 { + return validationResult(dbCache: dbCache, dbData: dbData) + } else { + + if attempts == 0 { + return Int32(mockValidateCombinedChainFailureHeight) + } else if attempts < 0 && mockValidateCombinedChainKeepFailing { + return Int32(mockValidateCombinedChainFailureHeight) + } else { + return validationResult(dbCache: dbCache, dbData: dbData) + } + } + } + return rustBackend.validateCombinedChain(dbCache: dbCache, dbData: dbData) + } + + private static func validationResult(dbCache: URL, dbData: URL) -> Int32{ + if mockDataDb { + return -1 + } else { + return rustBackend.validateCombinedChain(dbCache: dbCache, dbData: dbData) + } + } + + static func rewindToHeight(dbData: URL, height: Int32) -> Bool { + mockDataDb ? true : rustBackend.rewindToHeight(dbData: dbData, height: height) + } + + static func scanBlocks(dbCache: URL, dbData: URL) -> Bool { + if let rate = mockScanblocksSuccessRate { + + if shouldSucceed(successRate: rate) { + return mockDataDb ? true : rustBackend.scanBlocks(dbCache: dbCache, dbData: dbData) + } else { + return false + } + } + return rustBackend.scanBlocks(dbCache: dbCache, dbData: dbData) + } + + static func createToAddress(dbData: URL, account: Int32, extsk: String, consensusBranchId: Int32, to: String, value: Int64, memo: String?, spendParamsPath: String, outputParamsPath: String) -> Int64 { + mockCreateToAddress ?? rustBackend.createToAddress(dbData: dbData, account: account, extsk: extsk, consensusBranchId: consensusBranchId, to: to, value: value, memo: memo, spendParamsPath: spendParamsPath, outputParamsPath: outputParamsPath) + } + + static func shouldSucceed(successRate: Float) -> Bool { + let random = Float.random(in: 0.0...1.0) + return random <= successRate + } + + static func deriveExtendedFullViewingKey(_ spendingKey: String) throws -> String? { + nil + } + + static func deriveExtendedFullViewingKeys(seed: [UInt8], accounts: Int32) throws -> [String]? { + nil + } + + static func deriveExtendedSpendingKeys(seed: [UInt8], accounts: Int32) throws -> [String]? { + nil + } + + static func decryptAndStoreTransaction(dbData: URL, tx: [UInt8]) -> Bool { + false + } + + static func importExtendedFullViewingKey(dbData: URL, extfvk: String) throws -> Int32 { + -1 + } + + + static func consensusBranchIdFor(height: Int32) throws -> Int32 { + -1 + } + +} diff --git a/ZirclesTests/utils/TestCoordinator.swift b/ZirclesTests/utils/TestCoordinator.swift new file mode 100644 index 0000000..a5b3333 --- /dev/null +++ b/ZirclesTests/utils/TestCoordinator.swift @@ -0,0 +1,260 @@ +// +// TestCoordinator.swift +// ZcashLightClientKit-Unit-Tests +// +// Created by Francisco Gindre on 4/29/20. +// + +import Foundation +@testable import ZcashLightClientKit +@testable import Zircles +/** + This is the TestCoordinator + What does it do? quite a lot. + Is it a nice "SOLID" "Clean Code" piece of source code? + Hell no. It's your testing overlord and you will be grateful it is. + */ +class TestCoordinator { + + enum CoordinatorError: Error { + case notDarksideWallet + case notificationFromUnknownSynchronizer + case notMockLightWalletService + } + + enum SyncThreshold { + case upTo(height: BlockHeight) + case latestHeight + } + + enum DarksideData { + case `default` + case predefined(dataset: DarksideDataset) + case url(urlString: String, startHeigth: BlockHeight) + } + + var completionHandler: ((SDKSynchronizer) -> Void)? + var errorHandler: ((Error?) -> Void)? + var seed: String + var birthday: BlockHeight + var channelProvider: ChannelProvider + var synchronizer: SDKSynchronizer + var service: DarksideWalletService + var combineSynchronizer: CombineSynchronizer + var spendingKeys: [String]? + var databases: TemporaryTestDatabases + + init(seed: String, + walletBirthday: BlockHeight, + channelProvider: ChannelProvider) throws { + self.seed = seed + self.birthday = walletBirthday + self.channelProvider = channelProvider + self.databases = TemporaryDbBuilder.build() + self.service = DarksideWalletService() + let storage = CompactBlockStorage(url: databases.cacheDB, readonly: false) + try storage.createTable() + + let downloader = CompactBlockDownloader(service: self.service, storage: storage) + + + let buildResult = try TestSynchronizerBuilder.build( + rustBackend: ZcashRustBackend.self, + lowerBoundHeight: self.birthday, + cacheDbURL: databases.cacheDB, + dataDbURL: databases.dataDB, + pendingDbURL: databases.pendingDB, + service: self.service, + repository: TransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: databases.dataDB.absoluteString)), + downloader: downloader, + spendParamsURL: try __spendParamsURL(), + outputParamsURL: try __outputParamsURL(), + seedBytes: TestSeed().seed(), + walletBirthday: WalletBirthday.birthday(with: birthday), + loggerProxy: SampleLogger(logLevel: .debug)) + + self.synchronizer = buildResult.synchronizer.synchronizer + self.combineSynchronizer = buildResult.synchronizer + self.spendingKeys = buildResult.spendingKeys + subscribeToNotifications(synchronizer: self.synchronizer) + } + + func stop() throws { + try synchronizer.stop() + self.completionHandler = nil + self.errorHandler = nil + + } + + func setDarksideWalletState(_ state: DarksideData) throws { + + switch state { + case .default: + try service.useDataset(DarksideDataset.beforeReOrg.rawValue) + case .predefined(let dataset): + try service.useDataset(dataset.rawValue) + case .url(let urlString,_): + try service.useDataset(from: urlString) + } + + } + + func setLatestHeight(height: BlockHeight) throws { + try service.applyStaged(nextLatestHeight: height) + } + + func sync(completion: @escaping (SDKSynchronizer) -> Void, error: @escaping (Error?) -> Void) throws { + self.completionHandler = completion + self.errorHandler = error + + try synchronizer.start() + } + + + /** + Notifications + */ + func subscribeToNotifications(synchronizer: Synchronizer) { + + NotificationCenter.default.addObserver(self, selector: #selector(synchronizerFailed(_:)), name: .synchronizerFailed, object: synchronizer) + NotificationCenter.default.addObserver(self, selector: #selector(synchronizerSynced(_:)), name: .synchronizerSynced, object: synchronizer) + + } + + @objc func synchronizerFailed(_ notification: Notification) { + self.errorHandler?(notification.userInfo?[SDKSynchronizer.NotificationKeys.error] as? Error) + } + + @objc func synchronizerSynced(_ notification: Notification) { + if case .stopped = self.synchronizer.status { + LoggerProxy.debug("WARNING: notification received after synchronizer was stopped") + return + } + self.completionHandler?(self.synchronizer) + } + + @objc func synchronizerDisconnected(_ notification: Notification) { + /// TODO: See if we need hooks for this + } + + @objc func synchronizerStarted(_ notification: Notification) { + /// TODO: See if we need hooks for this + } + + @objc func synchronizerStopped(_ notification: Notification) { + /// TODO: See if we need hooks for this + } + + @objc func synchronizerSyncing(_ notification: Notification) { + /// TODO: See if we need hooks for this + } +} + +extension TestCoordinator { + func resetBlocks(dataset: DarksideData) throws { + + switch dataset { + case .default: + try service.useDataset(DarksideDataset.beforeReOrg.rawValue) + case .predefined(let blocks): + try service.useDataset(blocks.rawValue) + case .url(let urlString,_): + try service.useDataset(urlString) + } + } + + func stageBlockCreate(height: BlockHeight, count: Int = 1, nonce: Int = 0) throws { + try service.stageBlocksCreate(from: height, count: count, nonce: 0) + } + + func applyStaged(blockheight: BlockHeight) throws { + try service.applyStaged(nextLatestHeight: blockheight) + } + + func stageTransaction(_ tx: RawTransaction, at height: BlockHeight) throws { + try service.stageTransaction(tx, at: height) + } + + func stageTransaction(url: String, at height: BlockHeight) throws { + try service.stageTransaction(from: url, at: height) + } + + func latestHeight() throws -> BlockHeight { + try service.latestBlockHeight() + } + + func reset(saplingActivation: BlockHeight) throws { + try service.reset(saplingActivation: saplingActivation) + } + + func getIncomingTransactions() throws -> [RawTransaction]? { + return try service.getIncomingTransactions() + } +} + +struct TemporaryTestDatabases { + var cacheDB: URL + var dataDB: URL + var pendingDB: URL +} + +class TemporaryDbBuilder { + + static func build() -> TemporaryTestDatabases { + let tempUrl = try! __documentsDirectory() + let timestamp = String(Int(Date().timeIntervalSince1970)) + + return TemporaryTestDatabases(cacheDB: tempUrl.appendingPathComponent("cache_db_\(timestamp).db"), + dataDB: tempUrl.appendingPathComponent("data_db_\(timestamp).db"), + pendingDB: tempUrl.appendingPathComponent("pending_db_\(timestamp).db")) + } +} + +class TestSynchronizerBuilder { + + static func build( + rustBackend: ZcashRustBackendWelding.Type, + lowerBoundHeight: BlockHeight, + cacheDbURL: URL, + dataDbURL: URL, + pendingDbURL: URL, + service: LightWalletService, + repository: TransactionRepository, + downloader: CompactBlockDownloader, + spendParamsURL: URL, + outputParamsURL: URL, + seedBytes: [UInt8], + walletBirthday: WalletBirthday, + loggerProxy: Logger? = nil + ) throws -> (spendingKeys: [String]?, synchronizer: CombineSynchronizer) { + + let initializer = Initializer( + rustBackend: rustBackend, + lowerBoundHeight: lowerBoundHeight, + cacheDbURL: cacheDbURL, + dataDbURL: dataDbURL, + pendingDbURL: pendingDbURL, + service: service, + repository: repository, + downloader: downloader, + spendParamsURL: spendParamsURL, + outputParamsURL: outputParamsURL, + loggerProxy: loggerProxy + ) + let credentials = try initializer.initialize(seedProvider: StubSeedProvider(bytes: seedBytes), walletBirthdayHeight: walletBirthday.height) + + return (credentials, try CombineSynchronizer(initializer: initializer) + ) + } +} + +class StubSeedProvider: SeedProvider { + + let bytes: [UInt8] + init(bytes: [UInt8]) { + self.bytes = bytes + } + func seed() -> [UInt8] { + self.bytes + } +} diff --git a/ZirclesTests/utils/TestDbBuilder.swift b/ZirclesTests/utils/TestDbBuilder.swift new file mode 100644 index 0000000..89ab94e --- /dev/null +++ b/ZirclesTests/utils/TestDbBuilder.swift @@ -0,0 +1,143 @@ +// +// TestDbBuilder.swift +// ZcashLightClientKitTests +// +// Created by Francisco Gindre on 10/14/19. +// Copyright © 2019 Electric Coin Company. All rights reserved. +// + +import Foundation +import SQLite +@testable import ZcashLightClientKit + +struct TestDbHandle { + var originalDb: URL + var readWriteDb: URL + + init(originalDb: URL) { + self.originalDb = originalDb + self.readWriteDb = FileManager.default.temporaryDirectory.appendingPathComponent(self.originalDb.lastPathComponent.appending("_\(Date().timeIntervalSince1970)")) // avoid files clashing because crashing tests failed to remove previous ones by incrementally changing the filename + } + + func setUp() throws { + try FileManager.default.copyItem(at: originalDb, to: readWriteDb) + } + + func dispose() { + try? FileManager.default.removeItem(at: readWriteDb) + } + + func connectionProvider(readwrite: Bool = true) -> ConnectionProvider { + SimpleConnectionProvider(path: self.readWriteDb.absoluteString, readonly: !readwrite) + } +} + +class TestDbBuilder { + + enum TestBuilderError: Error { + case generalError + } + + static func inMemoryCompactBlockStorage() throws -> CompactBlockStorage { + let compactBlockDao = CompactBlockStorage(connectionProvider: try InMemoryDbProvider()) + try compactBlockDao.createTable() + return compactBlockDao + } + + static func diskCompactBlockStorage(at url: URL) throws -> CompactBlockStorage { + let compactBlockDao = CompactBlockStorage(connectionProvider: SimpleConnectionProvider(path: url.absoluteString)) + try compactBlockDao.createTable() + 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") + } + + static func prePopulatedDataDbURL() -> URL? { + Bundle(for: TestDbBuilder.self).url(forResource: "test_data", withExtension: "db") + } + + static func prepopulatedDataDbProvider() -> ConnectionProvider? { + let bundle = Bundle(for: TestDbBuilder.self) + guard let url = bundle.url(forResource: "ZcashSdk_Data", withExtension: "db") else { return nil } + let provider = SimpleConnectionProvider(path: url.absoluteString, readonly: true) + return provider + } + + static func transactionRepository() -> TransactionRepository? { + guard let provider = prepopulatedDataDbProvider() else { return nil } + + return TransactionSQLDAO(dbProvider: provider) + } + + static func sentNotesRepository() -> SentNotesRepository? { + guard let provider = prepopulatedDataDbProvider() else { return nil } + return SentNotesSQLDAO(dbProvider: provider) + } + + static func receivedNotesRepository() -> ReceivedNoteRepository? { + guard let provider = prepopulatedDataDbProvider() else { return nil } + return ReceivedNotesSQLDAO(dbProvider: provider) + } + + static func seed(db: CompactBlockRepository, with blockRange: CompactBlockRange) throws { + + guard let blocks = StubBlockCreator.createBlockRange(blockRange) else { + throw TestBuilderError.generalError + } + + try db.write(blocks: blocks) + } +} + +struct InMemoryDbProvider: ConnectionProvider { + var readonly: Bool + + var conn: Connection + init(readonly: Bool = false) throws { + self.readonly = readonly + self.conn = try Connection(.inMemory, readonly: readonly) + } + + func connection() throws -> Connection { + self.conn + } +} + +struct StubBlockCreator { + static func createRandomDataBlock(with height: BlockHeight) -> ZcashCompactBlock? { + guard let data = randomData(ofLength: 100) else { + LoggerProxy.debug("error creating stub block") + return nil + } + return ZcashCompactBlock(height: height, data: data) + } + static func createBlockRange(_ range: CompactBlockRange) -> [ZcashCompactBlock]? { + + var blocks = [ZcashCompactBlock]() + for height in range { + guard let block = createRandomDataBlock(with: height) else { + return nil + } + blocks.append(block) + } + + return blocks + } + + static func randomData(ofLength length: Int) -> Data? { + var bytes = [UInt8](repeating: 0, count: length) + let status = SecRandomCopyBytes(kSecRandomDefault, length, &bytes) + if status == errSecSuccess { + return Data(bytes: &bytes, count: bytes.count) + } + LoggerProxy.debug("Status \(status)") + return nil + + } + +} diff --git a/ZirclesTests/utils/Tests+Utils.swift b/ZirclesTests/utils/Tests+Utils.swift new file mode 100644 index 0000000..69d3bb5 --- /dev/null +++ b/ZirclesTests/utils/Tests+Utils.swift @@ -0,0 +1,125 @@ +// +// Tests+Utils.swift +// ZcashLightClientKitTests +// +// Created by Francisco Gindre on 18/09/2019. +// Copyright © 2019 Electric Coin Company. All rights reserved. +// + +import Foundation +import GRPC +import ZcashLightClientKit +import XCTest +import NIO +class LightWalletEndpointBuilder { + static var `default`: LightWalletEndpoint { + LightWalletEndpoint(address: "localhost", port: 9067, secure: false) + } +} + +class ChannelProvider { + func channel(secure: Bool = false) -> GRPCChannel { + let endpoint = LightWalletEndpointBuilder.default + + let configuration = ClientConnection.Configuration(target: .hostAndPort(endpoint.host, endpoint.port), eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1), tls: secure ? .init() : nil) + return ClientConnection(configuration: configuration) + + } +} + +struct MockDbInit { + @discardableResult static func emptyFile(at path: String) -> Bool { + + FileManager.default.createFile(atPath: path, contents: Data("".utf8), attributes: nil) + + } + + static func destroy(at path: String) throws { + try FileManager.default.removeItem(atPath: path) + } + +} + +extension XCTestExpectation { + func subscribe(to notification: Notification.Name, object: Any?) { + NotificationCenter.default.addObserver(self, selector: #selector(fulfill), name: notification, object: object) + } + + func unsubscribe(from notification: Notification.Name) { + NotificationCenter.default.removeObserver(self, name: notification, object: nil) + } + + func unsubscribeFromNotifications() { + NotificationCenter.default.removeObserver(self) + } +} + +func __documentsDirectory() throws -> URL { + try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) +} + +func __cacheDbURL() throws -> URL { + try __documentsDirectory().appendingPathComponent("cache.db", isDirectory: false) +} + +func __dataDbURL() throws -> URL { + try __documentsDirectory().appendingPathComponent("data.db", isDirectory: false) +} + +func __spendParamsURL() throws -> URL { + URL(string: Bundle.testBundle.url(forResource: "sapling-spend", withExtension: "params")!.path)! +} + +func __outputParamsURL() throws -> URL { + URL(string: Bundle.testBundle.url(forResource: "sapling-output", withExtension: "params")!.path)! +} + +func copyParametersToDocuments() throws -> (spend: URL, output: URL) { + + let spendURL = try __documentsDirectory().appendingPathComponent("sapling-spend.params", isDirectory: false) + let outputURL = try __documentsDirectory().appendingPathComponent("sapling-output.params", isDirectory: false) + try FileManager.default.copyItem(at: try __spendParamsURL(), to: spendURL) + try FileManager.default.copyItem(at: try __outputParamsURL(), to: outputURL) + + return (spendURL, outputURL) +} + +func deleteParametersFromDocuments() throws { + let documents = try __documentsDirectory() + deleteParamsFrom(spend: documents.appendingPathComponent("sapling-spend.params"), output: documents.appendingPathComponent("sapling-output.params")) +} +func deleteParamsFrom(spend: URL, output: URL) { + try? FileManager.default.removeItem(at: spend) + try? FileManager.default.removeItem(at: output) +} + +func parametersReady() -> Bool { + + guard let output = try? __outputParamsURL(), + let spend = try? __spendParamsURL(), + FileManager.default.isReadableFile(atPath: output.absoluteString), + FileManager.default.isReadableFile(atPath: spend.absoluteString) else { + return false + } + return true +} + +class StubTest: XCTestCase {} +extension Bundle { + static var testBundle: Bundle { + Bundle(for: StubTest.self) + } +} + + +class TestSeed: SeedProvider { + + /** + test account: "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" + */ + let seedString = Data(base64Encoded: "9VDVOZZZOWWHpZtq1Ebridp3Qeux5C+HwiRR0g7Oi7HgnMs8Gfln83+/Q1NnvClcaSwM4ADFL1uZHxypEWlWXg==")! + + func seed() -> [UInt8] { + [UInt8](seedString) + } +}