diff --git a/CHANGELOG.md b/CHANGELOG.md index adc086f8..27bb9f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). # Unreleased +### [#1379] Fulfill Payment from a valid ZIP-321 request +New API implemented that allows clients to use a ZIP-321 Payment URI to create transaction. +``` +func fulfillPaymentURI( + _ uri: String, + spendingKey: UnifiedSpendingKey + ) async throws -> ZcashTransaction.Overview +``` + +Possible errors: +- `ZcashError.rustProposeTransferFromURI` +- Other errors that `sentToAddress` can throw # 2.0.11 - 2024-03-08 diff --git a/Sources/ZcashLightClientKit/CombineSynchronizer.swift b/Sources/ZcashLightClientKit/CombineSynchronizer.swift index 324e5336..c6ed0880 100644 --- a/Sources/ZcashLightClientKit/CombineSynchronizer.swift +++ b/Sources/ZcashLightClientKit/CombineSynchronizer.swift @@ -96,14 +96,19 @@ public protocol CombineSynchronizer { toAddress: Recipient, memo: Memo? ) -> SinglePublisher - - @available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.") + + @available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients. use `proposeShielding:` instead") func shieldFunds( spendingKey: UnifiedSpendingKey, memo: Memo, shieldingThreshold: Zatoshi ) -> SinglePublisher + func proposefulfillingPaymentURI( + _ uri: String, + accountIndex: Int + ) -> SinglePublisher + var allTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get } var sentTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get } var receivedTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get } diff --git a/Sources/ZcashLightClientKit/Error/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index a0a99f1f..069ef7c3 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -321,6 +321,10 @@ public enum ZcashError: Equatable, Error { /// - `rustError` contains error generated by the rust layer. /// ZRUST0056 case rustGetWalletSummary(_ rustError: String) + /// Error from rust layer when calling ZcashRustBackend. + /// - `rustError` contains error generated by the rust layer. + /// ZRUST0057 + case rustProposeTransferFromURI(_ rustError: String) /// SQLite query failed when fetching all accounts from the database. /// - `sqliteError` is error produced by SQLite library. /// ZADAO0001 @@ -676,6 +680,7 @@ public enum ZcashError: Equatable, Error { case .rustLatestCachedBlockHeight: return "Error from rust layer when calling ZcashRustBackend.latestCachedBlockHeight" case .rustScanProgressOutOfRange: return "Rust layer's call ZcashRustBackend.getScanProgress returned values that after computation are outside of allowed range 0-100%." case .rustGetWalletSummary: return "Error from rust layer when calling ZcashRustBackend.getWalletSummary" + case .rustProposeTransferFromURI: return "Error from rust layer when calling ZcashRustBackend." case .accountDAOGetAll: return "SQLite query failed when fetching all accounts from the database." case .accountDAOGetAllCantDecode: return "Fetched accounts from SQLite but can't decode them." case .accountDAOFindBy: return "SQLite query failed when seaching for accounts in the database." @@ -850,6 +855,7 @@ public enum ZcashError: Equatable, Error { case .rustLatestCachedBlockHeight: return .rustLatestCachedBlockHeight case .rustScanProgressOutOfRange: return .rustScanProgressOutOfRange case .rustGetWalletSummary: return .rustGetWalletSummary + case .rustProposeTransferFromURI: return .rustProposeTransferFromURI case .accountDAOGetAll: return .accountDAOGetAll case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode case .accountDAOFindBy: return .accountDAOFindBy diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift index 24dbb96b..8290036a 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -175,6 +175,8 @@ public enum ZcashErrorCode: String { case rustScanProgressOutOfRange = "ZRUST0055" /// Error from rust layer when calling ZcashRustBackend.getWalletSummary case rustGetWalletSummary = "ZRUST0056" + /// Error from rust layer when calling ZcashRustBackend. + case rustProposeTransferFromURI = "ZRUST0057" /// SQLite query failed when fetching all accounts from the database. case accountDAOGetAll = "ZADAO0001" /// Fetched accounts from SQLite but can't decode them. diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift index 188577e7..de2c84b3 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift @@ -348,6 +348,10 @@ enum ZcashErrorDefinition { /// - `rustError` contains error generated by the rust layer. // sourcery: code="ZRUST0056" case rustGetWalletSummary(_ rustError: String) + /// Error from rust layer when calling ZcashRustBackend. + /// - `rustError` contains error generated by the rust layer. + // sourcery: code="ZRUST0057" + case rustProposeTransferFromURI(_ rustError: String) // MARK: - Account DAO diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index fa3b4a05..bb169dfd 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -112,6 +112,34 @@ actor ZcashRustBackend: ZcashRustBackendWelding { )) } + func proposeTransferFromURI( + _ uri: String, + account: Int32 + ) async throws -> FfiProposal { + globalDBLock.lock() + let proposal = zcashlc_propose_transfer_from_uri( + dbData.0, + dbData.1, + account, + [CChar](uri.utf8CString), + networkType.networkId, + minimumConfirmations, + useZIP317Fees + ) + globalDBLock.unlock() + + guard let proposal else { + throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`proposeTransfer` failed with unknown error")) + } + + defer { zcashlc_free_boxed_slice(proposal) } + + return try FfiProposal(contiguousBytes: Data( + bytes: proposal.pointee.ptr, + count: Int(proposal.pointee.len) + )) + } + func decryptAndStoreTransaction(txBytes: [UInt8], minedHeight: Int32) async throws { globalDBLock.lock() let result = zcashlc_decrypt_and_store_transaction( diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index 8843dea1..64e9793b 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -2,7 +2,7 @@ // ZcashRustBackendWelding.swift // ZcashLightClientKit // -// Created by Francisco Gindre on 12/09/2019. +// Created by Francisco 'Pacu' Gindre on 2019-12-09. // Copyright © 2019 Electric Coin Company. All rights reserved. // @@ -208,6 +208,21 @@ protocol ZcashRustBackendWelding { memo: MemoBytes? ) async throws -> FfiProposal + /// Select transaction inputs, compute fees, and construct a proposal for a transaction + /// that can then be authorized and made ready for submission to the network with + /// `createProposedTransaction` from a valid [ZIP-321](https://zips.z.cash/zip-0321) Payment Request UR + /// + /// - parameter uri: the URI String that the proposal will be made from. + /// - parameter account: index of the given account + /// - Parameter to: recipient address + /// - Parameter value: transaction amount in Zatoshi + /// - Parameter memo: the `MemoBytes` for this transaction. pass `nil` when sending to transparent receivers + /// - Throws: `rustCreateToAddress`. + func proposeTransferFromURI( + _ uri: String, + account: Int32 + ) async throws -> FfiProposal + /// Constructs a transaction proposal to shield all found UTXOs in data db for the given account, /// that can then be authorized and made ready for submission to the network with /// `createProposedTransaction`. diff --git a/Sources/ZcashLightClientKit/Synchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer.swift index e94013de..2e1788c6 100644 --- a/Sources/ZcashLightClientKit/Synchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer.swift @@ -215,7 +215,7 @@ public protocol Synchronizer: AnyObject { /// - Parameter toAddress: the recipient's address. /// - Parameter memo: an `Optional`with the memo to include as part of the transaction. send `nil` when sending to transparent receivers otherwise the function will throw an error /// - /// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws + /// - NOTE: If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws /// `SynchronizerErrors.notPrepared`. @available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.") func sendToAddress( @@ -225,12 +225,23 @@ public protocol Synchronizer: AnyObject { memo: Memo? ) async throws -> ZcashTransaction.Overview + /// Attempts to propose fulfilling a [ZIP-321](https://zips.z.cash/zip-0321) payment URI using the given `accountIndex` + /// - Parameter uri: a valid ZIP-321 payment URI + /// - Parameter accountIndex: the account index that allows spends to occur. + /// + /// - NOTE: If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws + /// `SynchronizerErrors.notPrepared`. + func proposefulfillingPaymentURI( + _ uri: String, + accountIndex: Int + ) async throws -> Proposal + /// Shields transparent funds from the given private key into the best shielded pool of the account associated to the given `UnifiedSpendingKey`. /// - Parameter spendingKey: the `UnifiedSpendingKey` that allows to spend transparent funds /// - Parameter memo: the optional memo to include as part of the transaction. /// - Parameter shieldingThreshold: the minimum transparent balance required before a transaction will be created. /// - /// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws + /// - Note: If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws /// `SynchronizerErrors.notPrepared`. @available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.") func shieldFunds( diff --git a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift index aa38be92..e93927a6 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift @@ -80,6 +80,18 @@ extension CombineSDKSynchronizer: CombineSynchronizer { } } + public func proposefulfillingPaymentURI( + _ uri: String, + accountIndex: Int + ) -> SinglePublisher { + AsyncToCombineGateway.executeThrowingAction() { + try await self.synchronizer.proposefulfillingPaymentURI( + uri, + accountIndex: accountIndex + ) + } + } + public func proposeShielding( accountIndex: Int, shieldingThreshold: Zatoshi, diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index a162a872..8025d9e0 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -12,6 +12,7 @@ import Combine /// Synchronizer implementation for UIKit and iOS 13+ // swiftlint:disable type_body_length public class SDKSynchronizer: Synchronizer { + public var alias: ZcashSynchronizerAlias { initializer.alias } private lazy var streamsUpdateQueue = { DispatchQueue(label: "streamsUpdateQueue_\(initializer.alias.description)") }() @@ -290,14 +291,29 @@ public class SDKSynchronizer: Synchronizer { ) async throws -> Proposal? { try throwIfUnprepared() - let proposal = try await transactionEncoder.proposeShielding( + return try await transactionEncoder.proposeShielding( accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memoBytes: memo.asMemoBytes(), transparentReceiver: transparentReceiver?.stringEncoded ) + } - return proposal + public func proposefulfillingPaymentURI( + _ uri: String, + accountIndex: Int + ) async throws -> Proposal { + do { + try throwIfUnprepared() + return try await transactionEncoder.proposeFulfillingPaymentFromURI( + uri, + accountIndex: accountIndex + ) + } catch ZcashError.rustCreateToAddress(let e) { + throw ZcashError.rustProposeTransferFromURI(e) + } catch { + throw error + } } public func createProposedTransactions( diff --git a/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift index 6acfef78..2deba5b4 100644 --- a/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift @@ -2,7 +2,7 @@ // TransactionEncoder.swift // ZcashLightClientKit // -// Created by Francisco Gindre on 11/20/19. +// Created by Francisco Gindre on 2019-11-20. // import Foundation @@ -72,6 +72,21 @@ protocol TransactionEncoder { spendingKey: UnifiedSpendingKey ) async throws -> [ZcashTransaction.Overview] + /// Creates a transaction proposal to fulfill a [ZIP-321](https://zips.z.cash/zip-0321), throwing an exception whenever things are missing. When the provided wallet implementation + /// doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using + /// double-bangs for things). + /// + /// - Parameters: + /// - Parameter uri: a valid ZIP-321 payment URI. + /// - Parameter accountIndex: the index of the account the proposal should be made from. + /// - Throws: + /// - `walletTransEncoderCreateTransactionMissingSaplingParams` if the sapling parameters aren't downloaded. + /// - Some `ZcashError.rust*` if the creation of transaction fails. + func proposeFulfillingPaymentFromURI( + _ uri: String, + accountIndex: Int + ) async throws -> Proposal + /// submits a transaction to the Zcash peer-to-peer network. /// - Parameter transaction: a transaction overview func submit(transaction: EncodedTransaction) async throws diff --git a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift index 31e4f796..3003270b 100644 --- a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift @@ -87,6 +87,17 @@ class WalletTransactionEncoder: TransactionEncoder { return Proposal(inner: proposal) } + func proposeFulfillingPaymentFromURI( + _ uri: String, + accountIndex: Int + ) async throws -> Proposal { + let proposal = try await rustBackend.proposeTransferFromURI( + uri, + account: Int32(accountIndex) + ) + return Proposal(inner: proposal) + } + func createProposedTransactions( proposal: Proposal, spendingKey: UnifiedSpendingKey diff --git a/Tests/DarksideTests/PaymentURIFulfillmentTests.swift b/Tests/DarksideTests/PaymentURIFulfillmentTests.swift new file mode 100644 index 00000000..22f54394 --- /dev/null +++ b/Tests/DarksideTests/PaymentURIFulfillmentTests.swift @@ -0,0 +1,342 @@ +// +// PaymentURIFulfillmentTests.swift +// DarksideTests +// +// Created by Francisco Gindre on 2024-02-19 +// + +import Combine +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +class PaymentURIFulfillmentTests: ZcashTestCase { + let sendAmount = Zatoshi(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(description: "reorg") + let branchID = "2bb40e60" + let chainName = "main" + let network = DarksideWalletDNetwork() + var cancellables: [AnyCancellable] = [] + + override func setUp() async throws { + try await super.setUp() + + // don't use an exact birthday, users never do. + self.coordinator = try await TestCoordinator( + container: mockContainer, + walletBirthday: birthday + 50, + network: network + ) + + try await coordinator.reset( + saplingActivation: 663150, + startSaplingTreeSize: 128607, + startOrchardTreeSize: 0, + branchID: self.branchID, + chainName: self.chainName + ) + } + + override func tearDown() async throws { + try await super.tearDown() + let coordinator = self.coordinator! + self.coordinator = nil + cancellables = [] + + try await coordinator.stop() + try? FileManager.default.removeItem(at: coordinator.databases.fsCacheDbRoot) + try? FileManager.default.removeItem(at: coordinator.databases.dataDB) + } + + /// Create a transaction from a ZIP-321 Payment URI + /// Pre-condition: Wallet has funds + /// + /// Steps: + /// 1. create fake chain + /// 1a. sync to latest height + /// 2. create proposal for PaymentURI + /// 3. getIncomingTransaction + /// 4. stage transaction at sentTxHeight + /// 5. applyHeight(sentTxHeight) + /// 6. sync to latest height + /// 7. stage 20 blocks from sentTxHeight + /// 8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg + /// 9. sync to latest height + /// 10. applyHeight(sentTxHeight + 2) + /// 10a. sync to latest height + /// 11. applyheight(sentTxHeight + 25) + func testPaymentToValidURIFulfillmentSucceeds() async throws { + /* + 1. create fake chain + */ + try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) + + try coordinator.applyStaged(blockheight: 663188) + sleep(2) + + let firstSyncExpectation = XCTestExpectation(description: "first sync") + /* + 1a. sync to latest height + */ + do { + try await coordinator.sync( + completion: { _ in + firstSyncExpectation.fulfill() + }, + error: self.handleError + ) + } catch { + await handleError(error) + } + + await fulfillment(of: [firstSyncExpectation], timeout: 5) + + sleep(1) + + let sendExpectation = XCTestExpectation(description: "send expectation") + var proposal: ZcashTransaction.Overview? + + /* + 2. send transaction to recipient address + */ + + let memo = "VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg" // "This is a simple memo." + let paymentURI = "zcash:\(Environment.testRecipientAddress)?amount=0.0002&memo=\(memo)&message=Thank%20you%20for%20your%20purchase&label=Your%20Purchase" + + do { + let proposal = try await coordinator.synchronizer.proposefulfillingPaymentURI( + paymentURI, + accountIndex: 0 + ) + + let transactions = try await coordinator.synchronizer.createProposedTransactions( + proposal: proposal, + spendingKey: coordinator.spendingKey + ) + + for try await tx in transactions { + switch tx { + case .grpcFailure(_, let error): + XCTFail("transaction failed to submit with error:\(error.localizedDescription)") + return + case .success(txId: let txId): + continue + case .submitFailure(txId: let txId, code: let code, description: let description): + XCTFail("transaction failed to submit with code: \(code) - description: \(description)") + return + case .notAttempted(txId: let txId): + XCTFail("transaction not attempted") + return + } + } + sendExpectation.fulfill() + } catch { + await handleError(error) + } + + await fulfillment(of: [sendExpectation], timeout: 13) + + + /** + 3. getIncomingTransaction + */ + guard let incomingTx = try coordinator.getIncomingTransactions()?.first else { + XCTFail("no incoming transaction") + try await coordinator.stop() + return + } + + let sentTxHeight: BlockHeight = 663189 + + /* + 4. stage transaction at sentTxHeight + */ + try coordinator.stageBlockCreate(height: sentTxHeight) + + try coordinator.stageTransaction(incomingTx, at: sentTxHeight) + + /* + 5. applyHeight(sentTxHeight) + */ + try coordinator.applyStaged(blockheight: sentTxHeight) + + sleep(2) + + /* + 6. sync to latest height + */ + let secondSyncExpectation = XCTestExpectation(description: "after send expectation") + + do { + try await coordinator.sync( + completion: { _ in + secondSyncExpectation.fulfill() + }, + error: self.handleError + ) + } catch { + await handleError(error) + } + + await fulfillment(of: [secondSyncExpectation], timeout: 5) + + /* + 7. stage 20 blocks from sentTxHeight + */ + try coordinator.stageBlockCreate(height: sentTxHeight, count: 25) + + /* + 7a. stage sent tx to sentTxHeight + 2 + */ + try coordinator.stageTransaction(incomingTx, at: sentTxHeight + 2) + + /* + 8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg + */ + try coordinator.applyStaged(blockheight: sentTxHeight + 1) + sleep(2) + + /* + 9. sync to latest height + */ + self.expectedReorgHeight = sentTxHeight + 1 + let afterReorgExpectation = XCTestExpectation(description: "after reorg sync") + + do { + try await coordinator.sync( + completion: { _ in + afterReorgExpectation.fulfill() + }, + error: self.handleError + ) + } catch { + await handleError(error) + } + + await fulfillment(of: [afterReorgExpectation], timeout: 5) + + // TODO: [#1247] needs to review this to properly solve, https://github.com/zcash/ZcashLightClientKit/issues/1247 + + /* + 10. applyHeight(sentTxHeight + 2) + */ + try coordinator.applyStaged(blockheight: sentTxHeight + 2) + sleep(2) + + let yetAnotherExpectation = XCTestExpectation(description: "after staging expectation") + + /* + 10a. sync to latest height + */ + do { + try await coordinator.sync( + completion: { _ in + yetAnotherExpectation.fulfill() + }, + error: self.handleError + ) + } catch { + await handleError(error) + } + + await fulfillment(of: [yetAnotherExpectation], timeout: 5) + + /* + 11. apply height(sentTxHeight + 25) + */ + try coordinator.applyStaged(blockheight: sentTxHeight + 25) + + sleep(2) + + let thisIsTheLastExpectationIPromess = XCTestExpectation(description: "last sync") + + /* + 12. sync to latest height + */ + do { + try await coordinator.sync( + completion: { _ in + thisIsTheLastExpectationIPromess.fulfill() + }, + error: self.handleError + ) + } catch { + await handleError(error) + } + + await fulfillment(of: [thisIsTheLastExpectationIPromess], timeout: 5) + } + + /// Attempt to create a transaction from an invalid ZIP-321 Payment URI and assert that fails + /// Pre-condition: Wallet has funds + /// + /// Steps: + /// 1. create fake chain + /// 1a. sync to latest height + /// 2. create proposal for PaymentURI + /// 3. check that fails + func testPaymentToInvalidURIFulfillmentFails() async throws { + /* + 1. create fake chain + */ + try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) + + try coordinator.applyStaged(blockheight: 663188) + sleep(2) + + let firstSyncExpectation = XCTestExpectation(description: "first sync") + /* + 1a. sync to latest height + */ + do { + try await coordinator.sync( + completion: { _ in + firstSyncExpectation.fulfill() + }, + error: self.handleError + ) + } catch { + await handleError(error) + } + + await fulfillment(of: [firstSyncExpectation], timeout: 5) + + sleep(1) + + /* + 2. send transaction to recipient address + */ + + let memo = "VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg" // "This is a simple memo." + let paymentURI = "zcash:zecIsGreat17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a?amount=0.0002&memo=\(memo)&message=Thank%20you%20for%20your%20purchase&label=Your%20Purchase" + + do { + let _ = try await coordinator.synchronizer.proposefulfillingPaymentURI( + paymentURI, + accountIndex: 0 + ) + + XCTFail("`fulfillPaymentURI` should have failed") + } catch ZcashError.rustProposeTransferFromURI { + XCTAssertTrue(true) + } catch { + XCTFail("Expected ZcashError.rustCreateToAddress but got \(error.localizedDescription)") + } + } + + func handleError(_ error: Error?) async { + _ = try? await coordinator.stop() + guard let testError = error else { + XCTFail("failed with nil error") + return + } + XCTFail("Failed with error: \(testError)") + } +} diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 156d078a..51612a02 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -1493,6 +1493,30 @@ class SynchronizerMock: Synchronizer { } } + // MARK: - proposefulfillingPaymentURI + + var proposefulfillingPaymentURIAccountIndexThrowableError: Error? + var proposefulfillingPaymentURIAccountIndexCallsCount = 0 + var proposefulfillingPaymentURIAccountIndexCalled: Bool { + return proposefulfillingPaymentURIAccountIndexCallsCount > 0 + } + var proposefulfillingPaymentURIAccountIndexReceivedArguments: (uri: String, accountIndex: Int)? + var proposefulfillingPaymentURIAccountIndexReturnValue: Proposal! + var proposefulfillingPaymentURIAccountIndexClosure: ((String, Int) async throws -> Proposal)? + + func proposefulfillingPaymentURI(_ uri: String, accountIndex: Int) async throws -> Proposal { + if let error = proposefulfillingPaymentURIAccountIndexThrowableError { + throw error + } + proposefulfillingPaymentURIAccountIndexCallsCount += 1 + proposefulfillingPaymentURIAccountIndexReceivedArguments = (uri: uri, accountIndex: accountIndex) + if let closure = proposefulfillingPaymentURIAccountIndexClosure { + return try await closure(uri, accountIndex) + } else { + return proposefulfillingPaymentURIAccountIndexReturnValue + } + } + // MARK: - shieldFunds var shieldFundsSpendingKeyMemoShieldingThresholdThrowableError: Error? @@ -2826,6 +2850,39 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } } + // MARK: - proposeTransferFromURI + + var proposeTransferFromURIAccountThrowableError: Error? + func setProposeTransferFromURIAccountThrowableError(_ param: Error?) async { + proposeTransferFromURIAccountThrowableError = param + } + var proposeTransferFromURIAccountCallsCount = 0 + var proposeTransferFromURIAccountCalled: Bool { + return proposeTransferFromURIAccountCallsCount > 0 + } + var proposeTransferFromURIAccountReceivedArguments: (uri: String, account: Int32)? + var proposeTransferFromURIAccountReturnValue: FfiProposal! + func setProposeTransferFromURIAccountReturnValue(_ param: FfiProposal) async { + proposeTransferFromURIAccountReturnValue = param + } + var proposeTransferFromURIAccountClosure: ((String, Int32) async throws -> FfiProposal)? + func setProposeTransferFromURIAccountClosure(_ param: ((String, Int32) async throws -> FfiProposal)?) async { + proposeTransferFromURIAccountClosure = param + } + + func proposeTransferFromURI(_ uri: String, account: Int32) async throws -> FfiProposal { + if let error = proposeTransferFromURIAccountThrowableError { + throw error + } + proposeTransferFromURIAccountCallsCount += 1 + proposeTransferFromURIAccountReceivedArguments = (uri: uri, account: account) + if let closure = proposeTransferFromURIAccountClosure { + return try await closure(uri, account) + } else { + return proposeTransferFromURIAccountReturnValue + } + } + // MARK: - proposeShielding var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError: Error?