ZcashLightClientKit/Tests/DarksideTests/BalanceTests.swift

1150 lines
46 KiB
Swift
Raw Normal View History

//
// BalanceTests.swift
// ZcashLightClientKit-Unit-Tests
//
// Created by Francisco Gindre on 4/28/20.
//
import XCTest
2022-02-28 09:03:20 -08:00
@testable import TestUtils
@testable import ZcashLightClientKit
2021-09-23 06:26:41 -07:00
// swiftlint:disable type_body_length implicitly_unwrapped_optional force_unwrapping file_length
class BalanceTests: XCTestCase {
2021-09-23 06:26:41 -07:00
// TODO: Parameterize this from environment?
// swiftlint:disable:next line_length
let 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"
let sendAmount = Zatoshi(1000)
let defaultLatestHeight: BlockHeight = 663188
2021-05-18 14:22:29 -07:00
let branchID = "2bb40e60"
let chainName = "main"
2021-07-28 09:59:10 -07:00
let network: ZcashNetwork = DarksideWalletDNetwork()
2021-09-23 06:26:41 -07:00
var birthday: BlockHeight = 663150
var sentTransactionExpectation = XCTestExpectation(description: "sent")
var syncedExpectation = XCTestExpectation(description: "synced")
var coordinator: TestCoordinator!
override func setUpWithError() throws {
2021-09-23 06:26:41 -07:00
try super.setUpWithError()
coordinator = try TestCoordinator(
seed: seedPhrase,
walletBirthday: birthday,
2021-07-28 09:59:10 -07:00
channelProvider: ChannelProvider(),
network: network
)
2021-05-18 14:22:29 -07:00
try coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
}
/**
2021-09-23 06:26:41 -07:00
verify that when sending the maximum amount, the transactions are broadcasted properly
*/
func testMaxAmountSend() throws {
let notificationHandler = SDKSynchonizerListener()
let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation")
let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation")
// 0 subscribe to updated transactions events
notificationHandler.subscribeToSynchronizer(coordinator.synchronizer)
// 1 sync and get spendable funds
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight + 10)
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: handleError)
wait(for: [firstSyncExpectation], timeout: 12)
// 2 check that there are no unconfirmed funds
let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
2021-07-28 09:59:10 -07:00
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
let maxBalance = verifiedBalance - network.constants.defaultFee(for: defaultLatestHeight)
// 3 create a transaction for the max amount possible
// 4 send the transaction
guard let spendingKey = coordinator.spendingKeys?.first else {
XCTFail("failed to create spending keys")
return
}
2021-09-23 06:26:41 -07:00
var pendingTx: PendingTransactionEntity?
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: maxBalance,
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
2022-09-08 16:44:38 -07:00
memo: try Memo(string: "this is a test"),
2021-09-23 06:26:41 -07:00
from: 0,
resultBlock: { result in
switch result {
case .failure(let error):
XCTFail("sendToAddress failed: \(error)")
case .success(let transaction):
pendingTx = transaction
}
self.sentTransactionExpectation.fulfill()
}
2021-09-23 06:26:41 -07:00
)
wait(for: [sentTransactionExpectation], timeout: 20)
guard let pendingTx = pendingTx else {
XCTFail("transaction creation failed")
return
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTAssertNotNil(transaction.rawTransactionId)
XCTAssertNotNil(pendingTx.rawTransactionId)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(transaction.rawTransactionId, pendingTx.rawTransactionId)
transactionMinedExpectation.fulfill()
}
// 5 apply to height
// 6 mine the block
guard let rawTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction after")
return
}
let latestHeight = try coordinator.latestHeight()
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
2021-09-23 06:26:41 -07:00
let foundTx = txs.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
foundTransactionsExpectation.fulfill()
}
try coordinator.stageBlockCreate(height: sentTxHeight, count: 100)
sleep(1)
try coordinator.stageTransaction(rawTx, at: sentTxHeight)
try coordinator.applyStaged(blockheight: sentTxHeight)
sleep(2) // add enhance breakpoint here
let mineExpectation = XCTestExpectation(description: "mineTxExpectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(
completion: { synchronizer in
let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now")
XCTAssertTrue(pendingEntity?.isMined ?? false)
XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight)
mineExpectation.fulfill()
},
error: { error in
guard let error = error else {
XCTFail("unknown error syncing after sending transaction")
return
}
XCTFail("Error: \(error)")
}
2021-09-23 06:26:41 -07:00
)
wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5)
// 7 advance to confirmation
try coordinator.applyStaged(blockheight: sentTxHeight + 10)
sleep(2)
let confirmExpectation = XCTestExpectation(description: "confirm expectation")
notificationHandler.transactionsFound = { txs in
XCTFail("We shouldn't find any transactions at this point but found \(txs)")
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)")
}
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
confirmExpectation.fulfill()
}, error: { e in
self.handleError(e)
})
wait(for: [confirmExpectation], timeout: 5)
2021-09-23 06:26:41 -07:00
let confirmedPending = try coordinator.synchronizer.allPendingTransactions()
.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNil(confirmedPending, "pending, now confirmed transaction found")
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), .zero)
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), .zero)
}
/**
2021-09-23 06:26:41 -07:00
verify that when sending the maximum amount minus one zatoshi, the transactions are broadcasted properly
*/
func testMaxAmountMinusOneSend() throws {
let notificationHandler = SDKSynchonizerListener()
let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation")
let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation")
// 0 subscribe to updated transactions events
notificationHandler.subscribeToSynchronizer(coordinator.synchronizer)
// 1 sync and get spendable funds
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight + 10)
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: handleError)
wait(for: [firstSyncExpectation], timeout: 12)
// 2 check that there are no unconfirmed funds
let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
2021-07-28 09:59:10 -07:00
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
let maxBalanceMinusOne = verifiedBalance - network.constants.defaultFee(for: defaultLatestHeight) - Zatoshi(1)
// 3 create a transaction for the max amount possible
// 4 send the transaction
guard let spendingKey = coordinator.spendingKeys?.first else {
XCTFail("failed to create spending keys")
return
}
2021-09-23 06:26:41 -07:00
var pendingTx: PendingTransactionEntity?
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: maxBalanceMinusOne,
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
memo: try Memo(string: "\(self.description) \(Date().description)"),
2021-09-23 06:26:41 -07:00
from: 0,
resultBlock: { result in
switch result {
case .failure(let error):
XCTFail("sendToAddress failed: \(error)")
case .success(let transaction):
pendingTx = transaction
}
self.sentTransactionExpectation.fulfill()
}
2021-09-23 06:26:41 -07:00
)
wait(for: [sentTransactionExpectation], timeout: 20)
guard let pendingTx = pendingTx else {
XCTFail("transaction creation failed")
return
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTAssertNotNil(transaction.rawTransactionId)
XCTAssertNotNil(pendingTx.rawTransactionId)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(transaction.rawTransactionId, pendingTx.rawTransactionId)
transactionMinedExpectation.fulfill()
}
// 5 apply to height
// 6 mine the block
guard let rawTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction after")
return
}
let latestHeight = try coordinator.latestHeight()
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
2021-09-23 06:26:41 -07:00
let foundTx = txs.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
foundTransactionsExpectation.fulfill()
}
try coordinator.stageBlockCreate(height: sentTxHeight, count: 100)
sleep(1)
try coordinator.stageTransaction(rawTx, at: sentTxHeight)
try coordinator.applyStaged(blockheight: sentTxHeight)
sleep(2) // add enhance breakpoint here
let mineExpectation = XCTestExpectation(description: "mineTxExpectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { synchronizer in
let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now")
XCTAssertTrue(pendingEntity?.isMined ?? false)
XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight)
mineExpectation.fulfill()
2021-09-23 06:26:41 -07:00
}, error: { error in
guard let e = error else {
XCTFail("unknown error syncing after sending transaction")
return
}
XCTFail("Error: \(e)")
})
wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5)
// 7 advance to confirmation
try coordinator.applyStaged(blockheight: sentTxHeight + 10)
sleep(2)
let confirmExpectation = XCTestExpectation(description: "confirm expectation")
notificationHandler.transactionsFound = { txs in
XCTFail("We shouldn't find any transactions at this point but found \(txs)")
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)")
}
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
confirmExpectation.fulfill()
}, error: { e in
self.handleError(e)
})
wait(for: [confirmExpectation], timeout: 5)
2021-09-23 06:26:41 -07:00
let confirmedPending = try coordinator.synchronizer
.allPendingTransactions()
.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNil(confirmedPending, "pending, now confirmed transaction found")
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), Zatoshi(1))
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), Zatoshi(1))
}
/**
2021-09-23 06:26:41 -07:00
verify that when sending the a no change transaction, the transactions are broadcasted properly
*/
func testSingleNoteNoChangeTransaction() throws {
let notificationHandler = SDKSynchonizerListener()
let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation")
let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation")
// 0 subscribe to updated transactions events
notificationHandler.subscribeToSynchronizer(coordinator.synchronizer)
// 1 sync and get spendable funds
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight + 10)
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: handleError)
wait(for: [firstSyncExpectation], timeout: 12)
// 2 check that there are no unconfirmed funds
let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
2021-07-28 09:59:10 -07:00
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
let maxBalanceMinusOne = Zatoshi(100000) - network.constants.defaultFee(for: defaultLatestHeight)
// 3 create a transaction for the max amount possible
// 4 send the transaction
guard let spendingKey = coordinator.spendingKeys?.first else {
XCTFail("failed to create spending keys")
return
}
var pendingTx: PendingTransactionEntity?
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: maxBalanceMinusOne,
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
memo: try Memo(string: "test send \(self.description) \(Date().description)"),
2021-09-23 06:26:41 -07:00
from: 0,
resultBlock: { result in
switch result {
case .failure(let error):
XCTFail("sendToAddress failed: \(error)")
case .success(let transaction):
pendingTx = transaction
}
self.sentTransactionExpectation.fulfill()
}
2021-09-23 06:26:41 -07:00
)
wait(for: [sentTransactionExpectation], timeout: 20)
guard let pendingTx = pendingTx else {
XCTFail("transaction creation failed")
return
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTAssertNotNil(transaction.rawTransactionId)
XCTAssertNotNil(pendingTx.rawTransactionId)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(transaction.rawTransactionId, pendingTx.rawTransactionId)
transactionMinedExpectation.fulfill()
}
// 5 apply to height
// 6 mine the block
guard let rawTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction after")
return
}
let latestHeight = try coordinator.latestHeight()
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
2021-09-23 06:26:41 -07:00
let foundTx = txs.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
foundTransactionsExpectation.fulfill()
}
try coordinator.stageBlockCreate(height: sentTxHeight, count: 100)
sleep(1)
try coordinator.stageTransaction(rawTx, at: sentTxHeight)
try coordinator.applyStaged(blockheight: sentTxHeight)
sleep(2) // add enhance breakpoint here
let mineExpectation = XCTestExpectation(description: "mineTxExpectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { synchronizer in
let pendingEntity = synchronizer.pendingTransactions.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNotNil(pendingEntity, "pending transaction should have been mined by now")
XCTAssertTrue(pendingEntity?.isMined ?? false)
XCTAssertEqual(pendingEntity?.minedHeight, sentTxHeight)
mineExpectation.fulfill()
2021-09-23 06:26:41 -07:00
}, error: { error in
guard let e = error else {
XCTFail("unknown error syncing after sending transaction")
return
}
XCTFail("Error: \(e)")
})
wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5)
// 7 advance to confirmation
try coordinator.applyStaged(blockheight: sentTxHeight + 10)
sleep(2)
let confirmExpectation = XCTestExpectation(description: "confirm expectation")
notificationHandler.transactionsFound = { txs in
XCTFail("We shouldn't find any transactions at this point but found \(txs)")
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)")
}
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
confirmExpectation.fulfill()
}, error: { e in
self.handleError(e)
})
wait(for: [confirmExpectation], timeout: 5)
2021-09-23 06:26:41 -07:00
let confirmedPending = try coordinator.synchronizer
.allPendingTransactions()
.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNil(confirmedPending, "pending, now confirmed transaction found")
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), Zatoshi(100000))
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), Zatoshi(100000))
}
2021-09-23 06:26:41 -07:00
/**
2021-09-23 06:26:41 -07:00
Verify available balance is correct in all wallet states during a send
This can be either a Wallet test or a Synchronizer test. The latter is supposed to be simpler because it involves no UI testing whatsoever.
Precondition:
Account has spendable funds
Librustzcash is synced up to current tip
Action:
Send Amount(*) to zAddr
Success per state:
Sent: (previous available funds - spent note + change) equals to (previous available funds - sent amount)
Error: previous available funds equals to current funds
*/
// swiftlint:disable cyclomatic_complexity
func testVerifyAvailableBalanceDuringSend() throws {
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight)
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
self.syncedExpectation.fulfill()
}, error: handleError)
wait(for: [syncedExpectation], timeout: 60)
guard let spendingKey = coordinator.spendingKeys?.first else {
XCTFail("failed to create spending keys")
return
}
let presendVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
/*
2021-09-23 06:26:41 -07:00
there's more zatoshi to send than network fee
*/
XCTAssertTrue(presendVerifiedBalance >= network.constants.defaultFee(for: defaultLatestHeight) + sendAmount)
var pendingTx: PendingTransactionEntity?
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: sendAmount,
2022-09-08 16:44:38 -07:00
toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
memo: try Memo(string: "this is a test"),
2021-09-23 06:26:41 -07:00
from: 0,
resultBlock: { result in
switch result {
case .failure(let error):
/*
balance should be the same as before sending if transaction failed
*/
XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), presendVerifiedBalance)
XCTFail("sendToAddress failed: \(error)")
case .success(let transaction):
pendingTx = transaction
}
self.sentTransactionExpectation.fulfill()
}
2021-09-23 06:26:41 -07:00
)
XCTAssertTrue(coordinator.synchronizer.initializer.getVerifiedBalance() > .zero)
wait(for: [sentTransactionExpectation], timeout: 12)
// sync and mine
guard let rawTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction after")
return
}
let latestHeight = try coordinator.latestHeight()
let sentTxHeight = latestHeight + 1
try coordinator.stageBlockCreate(height: sentTxHeight)
try coordinator.stageTransaction(rawTx, at: sentTxHeight)
try coordinator.applyStaged(blockheight: sentTxHeight)
sleep(1)
let mineExpectation = XCTestExpectation(description: "mineTxExpectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
mineExpectation.fulfill()
2021-09-23 06:26:41 -07:00
}, error: { error in
guard let e = error else {
XCTFail("unknown error syncing after sending transaction")
return
}
XCTFail("Error: \(e)")
})
wait(for: [mineExpectation], timeout: 5)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(
presendVerifiedBalance - self.sendAmount - network.constants.defaultFee(for: defaultLatestHeight),
coordinator.synchronizer.initializer.getBalance()
)
XCTAssertEqual(
presendVerifiedBalance - self.sendAmount - network.constants.defaultFee(for: defaultLatestHeight),
coordinator.synchronizer.initializer.getVerifiedBalance()
)
guard let transaction = pendingTx else {
XCTFail("pending transaction nil")
return
}
2021-09-23 06:26:41 -07:00
/*
2021-09-23 06:26:41 -07:00
basic health check
*/
XCTAssertEqual(transaction.value, self.sendAmount)
/*
2021-09-23 06:26:41 -07:00
build up repos to get data
*/
guard let txid = transaction.rawTransactionId else {
XCTFail("sent transaction has no internal id")
return
}
2021-09-23 06:26:41 -07:00
let sentNoteDAO = SentNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: self.coordinator.synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
)
)
2021-09-23 06:26:41 -07:00
let receivedNoteDAO = ReceivedNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: self.coordinator.synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
)
)
var sentEntity: SentNoteEntity?
do {
2021-09-23 06:26:41 -07:00
sentEntity = try sentNoteDAO.sentNote(byRawTransactionId: txid)
} catch {
XCTFail("error retrieving sent note: \(error)")
}
2021-09-23 06:26:41 -07:00
guard let sentNote = sentEntity else {
XCTFail("could not find sent note for this transaction")
return
}
2021-09-23 06:26:41 -07:00
var receivedEntity: ReceivedNoteEntity?
do {
2021-09-23 06:26:41 -07:00
receivedEntity = try receivedNoteDAO.receivedNote(byRawTransactionId: txid)
} catch {
XCTFail("error retrieving received note: \(error)")
}
2021-09-23 06:26:41 -07:00
guard let receivedNote = receivedEntity else {
XCTFail("could not find sent note for this transaction")
return
}
2021-09-23 06:26:41 -07:00
// (previous available funds - spent note + change) equals to (previous available funds - sent amount)
2021-09-23 06:26:41 -07:00
self.verifiedBalanceValidation(
previousBalance: presendVerifiedBalance,
spentNoteValue: Zatoshi(Int64(sentNote.value)),
changeValue: Zatoshi(Int64(receivedNote.value)),
sentAmount: self.sendAmount,
2021-09-23 06:26:41 -07:00
currentVerifiedBalance: self.coordinator.synchronizer.initializer.getVerifiedBalance()
)
}
/**
2021-09-23 06:26:41 -07:00
Verify total balance in all wallet states during a send
This can be either a Wallet test or a Synchronizer test. The latter is supposed to be simpler because it involves no UI testing whatsoever.
Precondition:
Account has spendable funds
Librustzcash is synced up to current tip
Action:
Send Amount to zAddr
Success per state:
Sent: (total balance funds - sentAmount) equals to (previous available funds - sent amount)
Error: previous total balance funds equals to current total balance
*/
func testVerifyTotalBalanceDuringSend() throws {
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight)
sleep(2)
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
self.syncedExpectation.fulfill()
}, error: handleError)
wait(for: [syncedExpectation], timeout: 5)
guard let spendingKey = coordinator.spendingKeys?.first else {
XCTFail("failed to create spending keys")
return
}
let presendBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
2021-09-23 06:26:41 -07:00
// there's more zatoshi to send than network fee
XCTAssertTrue(presendBalance >= network.constants.defaultFee(for: defaultLatestHeight) + sendAmount)
var pendingTx: PendingTransactionEntity?
var error: Error?
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: sendAmount,
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
memo: try Memo(string: "test send \(self.description) \(Date().description)"),
2021-09-23 06:26:41 -07:00
from: 0,
resultBlock: { result in
switch result {
case .failure(let e):
// balance should be the same as before sending if transaction failed
error = e
XCTFail("sendToAddress failed: \(e)")
case .success(let transaction):
pendingTx = transaction
}
self.sentTransactionExpectation.fulfill()
}
2021-09-23 06:26:41 -07:00
)
XCTAssertTrue(coordinator.synchronizer.initializer.getVerifiedBalance() > .zero)
wait(for: [sentTransactionExpectation], timeout: 12)
if let e = error {
XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), presendBalance)
XCTFail("error: \(e)")
return
}
guard let transaction = pendingTx else {
XCTFail("pending transaction nil after send")
return
}
XCTAssertEqual(transaction.value, self.sendAmount)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(
self.coordinator.synchronizer.initializer.getBalance(),
presendBalance - self.sendAmount - network.constants.defaultFee(for: defaultLatestHeight)
2021-09-23 06:26:41 -07:00
)
XCTAssertNil(transaction.errorCode)
let latestHeight = try coordinator.latestHeight()
let sentTxHeight = latestHeight + 1
try coordinator.stageBlockCreate(height: sentTxHeight)
guard let rawTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction after send")
return
}
2021-09-23 06:26:41 -07:00
try coordinator.stageTransaction(rawTx, at: latestHeight + 1)
try coordinator.applyStaged(blockheight: latestHeight + 1)
sleep(2)
let mineExpectation = XCTestExpectation(description: "mineTxExpectation")
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
mineExpectation.fulfill()
2021-09-23 06:26:41 -07:00
}, error: { error in
guard let e = error else {
XCTFail("unknown error syncing after sending transaction")
return
}
XCTFail("Error: \(e)")
})
wait(for: [mineExpectation], timeout: 5)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(
presendBalance - self.sendAmount - network.constants.defaultFee(for: defaultLatestHeight),
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.initializer.getBalance()
)
}
/**
2021-09-23 06:26:41 -07:00
Verify incoming transactions
This can be either a Wallet test or a Synchronizer test. The latter is supposed to be simpler because it involves no UI testing whatsoever.
2021-09-23 06:26:41 -07:00
Precondition:
Librustzcash is synced up to current tip
Known list of expected transactions on the block range to sync the wallet up to.
Known expected balance on the block range to sync the wallet up to.
Action:
sync to latest height
Success criteria:
The transaction list matches the expected one
Balance matches expected balance
*/
func testVerifyIncomingTransaction() throws {
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight)
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
self.syncedExpectation.fulfill()
}, error: self.handleError)
wait(for: [syncedExpectation], timeout: 5)
XCTAssertEqual(coordinator.synchronizer.clearedTransactions.count, 2)
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), Zatoshi(200000))
}
/**
2021-09-23 06:26:41 -07:00
Verify change transactions
This can be either a Wallet test or a Synchronizer test. The latter is supposed to be simpler because it involves no UI testing whatsoever.
Precondition
Librustzcash is synced up to current tip
Known list of expected transactions on the block range to sync the wallet up to.
Known expected balance on the block range to sync the wallet up to.
Theres a spendable note with value > send amount that generates change
Action:
Send amount to zAddr
sync to minedHeight + 1
Success Criteria:
Theres a sent transaction matching the amount sent to the given zAddr
minedHeight is not -1
Balance meets verified Balance and total balance criteria
Theres a change note of value (previous note value - sent amount)
*/
func testVerifyChangeTransaction() throws {
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildSingleNoteChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight)
let sendExpectation = XCTestExpectation(description: "send expectation")
let createToAddressExpectation = XCTestExpectation(description: "create to address")
try coordinator.setLatestHeight(height: defaultLatestHeight)
2021-09-23 06:26:41 -07:00
/*
2021-09-23 06:26:41 -07:00
sync to current tip
*/
try coordinator.sync(completion: { _ in
self.syncedExpectation.fulfill()
}, error: self.handleError)
wait(for: [syncedExpectation], timeout: 6)
let previousVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let previousTotalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
guard let spendingKeys = coordinator.spendingKeys?.first else {
XCTFail("null spending keys")
return
}
/*
2021-09-23 06:26:41 -07:00
Send
*/
let memo = try Memo(string: "shielding is fun!")
var pendingTx: PendingTransactionEntity?
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.sendToAddress(
spendingKey: spendingKeys,
zatoshi: sendAmount,
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
2021-09-23 06:26:41 -07:00
memo: memo,
from: 0,
resultBlock: { sendResult in
DispatchQueue.main.async {
switch sendResult {
case .failure(let sendError):
XCTFail("error sending \(sendError)")
case .success(let transaction):
pendingTx = transaction
}
sendExpectation.fulfill()
}
}
2021-09-23 06:26:41 -07:00
)
wait(for: [createToAddressExpectation], timeout: 30)
let syncToMinedheightExpectation = XCTestExpectation(description: "sync to mined height + 1")
/*
2021-09-23 06:26:41 -07:00
include sent transaction in block
*/
guard let rawTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("pending transaction nil after send")
return
}
let latestHeight = try coordinator.latestHeight()
let sentTxHeight = latestHeight + 1
try coordinator.stageBlockCreate(height: sentTxHeight, count: 12)
try coordinator.stageTransaction(rawTx, at: sentTxHeight)
try coordinator.applyStaged(blockheight: sentTxHeight + 11 )
sleep(2)
/*
2021-09-23 06:26:41 -07:00
Sync to that block
*/
try coordinator.sync(
completion: { synchronizer in
let confirmedTx: ConfirmedTransactionEntity!
do {
confirmedTx = try synchronizer.allClearedTransactions().first(where: { confirmed -> Bool in
confirmed.transactionEntity.transactionId == pendingTx?.transactionEntity.transactionId
})
} catch {
XCTFail("Error retrieving cleared transactions")
return
}
/*
Theres a sent transaction matching the amount sent to the given zAddr
*/
XCTAssertEqual(confirmedTx.value, self.sendAmount)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(confirmedTx.toAddress, self.testRecipientAddress)
do {
let confirmedMemo = try confirmedTx.memo.intoMemoBytes().intoMemo()
XCTAssertEqual(confirmedMemo, memo)
} catch {
XCTFail("failed retrieving memo from confirmed transaction. Error: \(error.localizedDescription)")
}
2021-09-23 06:26:41 -07:00
guard let transactionId = confirmedTx.rawTransactionId else {
XCTFail("no raw transaction id")
return
}
/*
Find out what note was used
*/
let sentNotesRepo = SentNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
)
)
guard let sentNote = try? sentNotesRepo.sentNote(byRawTransactionId: transactionId) else {
XCTFail("Could not finde sent note with transaction Id \(transactionId)")
return
}
let receivedNotesRepo = ReceivedNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: self.coordinator.synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
)
)
/*
get change note
*/
guard let receivedNote = try? receivedNotesRepo.receivedNote(byRawTransactionId: transactionId) else {
XCTFail("Could not find received not with change for transaction Id \(transactionId)")
return
}
/*
Theres a change note of value (previous note value - sent amount)
*/
XCTAssertEqual(
previousVerifiedBalance - self.sendAmount - self.network.constants.defaultFee(for: self.defaultLatestHeight),
Zatoshi(Int64(receivedNote.value))
2021-09-23 06:26:41 -07:00
)
/*
Balance meets verified Balance and total balance criteria
*/
self.verifiedBalanceValidation(
previousBalance: previousVerifiedBalance,
spentNoteValue: Zatoshi(Int64(sentNote.value)),
changeValue: Zatoshi(Int64(receivedNote.value)),
sentAmount: self.sendAmount,
2021-09-23 06:26:41 -07:00
currentVerifiedBalance: synchronizer.initializer.getVerifiedBalance()
)
self.totalBalanceValidation(
totalBalance: synchronizer.initializer.getBalance(),
previousTotalbalance: previousTotalBalance,
sentAmount: self.sendAmount
2021-09-23 06:26:41 -07:00
)
syncToMinedheightExpectation.fulfill()
},
error: self.handleError
)
wait(for: [syncToMinedheightExpectation], timeout: 5)
}
/**
2021-09-23 06:26:41 -07:00
erify transactions that expire are reflected accurately in balance
This test requires the transaction to expire.
How can we mock or cause this? Would createToAddress and faking a network submission through lightwalletService and syncing 10 more blocks work?
2021-09-23 06:26:41 -07:00
Precondition:
Account has spendable funds
Librustzcash is synced up to current tip
Current tip can be scanned 10 blocks past the generated to be expired transaction
Action:
Sync to current tip
Create transaction to zAddr
Mock send success
Sync 10 blocks more
Success Criteria:
Theres a pending transaction that has expired
Total Balance is equal to total balance previously shown before sending the expired transaction
Verified Balance is equal to verified balance previously shown before sending the expired transaction
2021-09-23 06:26:41 -07:00
*/
func testVerifyBalanceAfterExpiredTransaction() throws {
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: self.defaultLatestHeight)
sleep(2)
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
self.syncedExpectation.fulfill()
}, error: self.handleError)
wait(for: [syncedExpectation], timeout: 5)
guard let spendingKey = coordinator.spendingKeys?.first else {
XCTFail("no synchronizer or spending keys")
return
}
let previousVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let previousTotalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
let sendExpectation = XCTestExpectation(description: "send expectation")
var pendingTx: PendingTransactionEntity?
2021-09-23 06:26:41 -07:00
coordinator.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: sendAmount,
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
memo: try Memo(string: "test send \(self.description)"),
2021-09-23 06:26:41 -07:00
from: 0,
resultBlock: { result in
switch result {
case .failure(let error):
// balance should be the same as before sending if transaction failed
XCTAssertEqual(self.coordinator.synchronizer.initializer.getVerifiedBalance(), previousVerifiedBalance)
XCTAssertEqual(self.coordinator.synchronizer.initializer.getBalance(), previousTotalBalance)
XCTFail("sendToAddress failed: \(error)")
case .success(let pending):
pendingTx = pending
}
}
2021-09-23 06:26:41 -07:00
)
wait(for: [sendExpectation], timeout: 12)
guard let pendingTransaction = pendingTx, pendingTransaction.expiryHeight > defaultLatestHeight else {
XCTFail("No pending transaction")
return
}
let expirationSyncExpectation = XCTestExpectation(description: "expiration sync expectation")
let expiryHeight = pendingTransaction.expiryHeight
let blockCount = abs(self.defaultLatestHeight - expiryHeight)
try coordinator.stageBlockCreate(height: self.defaultLatestHeight + 1, count: blockCount)
try coordinator.applyStaged(blockheight: expiryHeight + 1)
sleep(2)
2021-09-23 06:26:41 -07:00
try coordinator.sync(completion: { _ in
expirationSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [expirationSyncExpectation], timeout: 5)
/*
2021-09-23 06:26:41 -07:00
Verified Balance is equal to verified balance previously shown before sending the expired transaction
*/
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), previousVerifiedBalance)
/*
2021-09-23 06:26:41 -07:00
Total Balance is equal to total balance previously shown before sending the expired transaction
*/
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), previousTotalBalance)
2021-09-23 06:26:41 -07:00
let pendingRepo = PendingTransactionSQLDAO(
dbProvider: SimpleConnectionProvider(
path: coordinator.synchronizer.initializer.pendingDbURL.absoluteString
)
)
guard let expiredPending = try? pendingRepo.find(by: pendingTransaction.id!),
let id = expiredPending.id else {
XCTFail("pending transaction not found")
return
}
/*
2021-09-23 06:26:41 -07:00
there no sent transaction displayed
*/
XCTAssertNil( try coordinator.synchronizer.allSentTransactions().first(where: { $0.id == id }))
/*
2021-09-23 06:26:41 -07:00
Theres a pending transaction that has expired
*/
XCTAssertEqual(expiredPending.minedHeight, -1)
}
func handleError(_ error: Error?) {
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
}
/**
2021-09-23 06:26:41 -07:00
check if (previous available funds - spent note + change) equals to (previous available funds - sent amount)
*/
func verifiedBalanceValidation(
previousBalance: Zatoshi,
spentNoteValue: Zatoshi,
changeValue: Zatoshi,
sentAmount: Zatoshi,
currentVerifiedBalance: Zatoshi
2021-09-23 06:26:41 -07:00
) {
XCTAssertEqual(previousBalance - spentNoteValue + changeValue, currentVerifiedBalance - sentAmount)
}
2021-09-23 06:26:41 -07:00
func totalBalanceValidation(
totalBalance: Zatoshi,
previousTotalbalance: Zatoshi,
sentAmount: Zatoshi
2021-09-23 06:26:41 -07:00
) {
XCTAssertEqual(totalBalance, previousTotalbalance - sentAmount - network.constants.defaultFee(for: defaultLatestHeight))
}
}
class SDKSynchonizerListener {
2021-09-23 06:26:41 -07:00
var transactionsFound: (([ConfirmedTransactionEntity]) -> Void)?
var synchronizerMinedTransaction: ((PendingTransactionEntity) -> Void)?
func subscribeToSynchronizer(_ synchronizer: SDKSynchronizer) {
NotificationCenter.default.addObserver(self, selector: #selector(txFound(_:)), name: .synchronizerFoundTransactions, object: synchronizer)
NotificationCenter.default.addObserver(self, selector: #selector(txMined(_:)), name: .synchronizerMinedTransaction, object: synchronizer)
}
func unsubscribe() {
NotificationCenter.default.removeObserver(self)
}
@objc func txFound(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let txs = notification.userInfo?[SDKSynchronizer.NotificationKeys.foundTransactions] as? [ConfirmedTransactionEntity] else {
XCTFail("expected [ConfirmedTransactionEntity] array")
return
}
self?.transactionsFound?(txs)
}
}
@objc func txMined(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
2021-09-23 06:26:41 -07:00
guard let transaction = notification.userInfo?[SDKSynchronizer.NotificationKeys.minedTransaction] as? PendingTransactionEntity else {
XCTFail("expected transaction")
return
}
2021-09-23 06:26:41 -07:00
self?.synchronizerMinedTransaction?(transaction)
}
}
}