[#1379] Fulfill Payment from a valid ZIP-321 request
Closes #1379 Adds test for parsing error Adds test for successful tx creation Public API changes in CHANGELOG.md
This commit is contained in:
parent
6c9b7a91d6
commit
c873e208c4
12
CHANGELOG.md
12
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).
|
and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
# Unreleased
|
# 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
|
# 2.0.11 - 2024-03-08
|
||||||
|
|
||||||
|
|
|
@ -96,14 +96,19 @@ public protocol CombineSynchronizer {
|
||||||
toAddress: Recipient,
|
toAddress: Recipient,
|
||||||
memo: Memo?
|
memo: Memo?
|
||||||
) -> SinglePublisher<ZcashTransaction.Overview, Error>
|
) -> SinglePublisher<ZcashTransaction.Overview, Error>
|
||||||
|
|
||||||
@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(
|
func shieldFunds(
|
||||||
spendingKey: UnifiedSpendingKey,
|
spendingKey: UnifiedSpendingKey,
|
||||||
memo: Memo,
|
memo: Memo,
|
||||||
shieldingThreshold: Zatoshi
|
shieldingThreshold: Zatoshi
|
||||||
) -> SinglePublisher<ZcashTransaction.Overview, Error>
|
) -> SinglePublisher<ZcashTransaction.Overview, Error>
|
||||||
|
|
||||||
|
func proposefulfillingPaymentURI(
|
||||||
|
_ uri: String,
|
||||||
|
accountIndex: Int
|
||||||
|
) -> SinglePublisher<Proposal, Error>
|
||||||
|
|
||||||
var allTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get }
|
var allTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get }
|
||||||
var sentTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get }
|
var sentTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get }
|
||||||
var receivedTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get }
|
var receivedTransactions: SinglePublisher<[ZcashTransaction.Overview], Never> { get }
|
||||||
|
|
|
@ -321,6 +321,10 @@ public enum ZcashError: Equatable, Error {
|
||||||
/// - `rustError` contains error generated by the rust layer.
|
/// - `rustError` contains error generated by the rust layer.
|
||||||
/// ZRUST0056
|
/// ZRUST0056
|
||||||
case rustGetWalletSummary(_ rustError: String)
|
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.
|
/// SQLite query failed when fetching all accounts from the database.
|
||||||
/// - `sqliteError` is error produced by SQLite library.
|
/// - `sqliteError` is error produced by SQLite library.
|
||||||
/// ZADAO0001
|
/// ZADAO0001
|
||||||
|
@ -676,6 +680,7 @@ public enum ZcashError: Equatable, Error {
|
||||||
case .rustLatestCachedBlockHeight: return "Error from rust layer when calling ZcashRustBackend.latestCachedBlockHeight"
|
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 .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 .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 .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 .accountDAOGetAllCantDecode: return "Fetched accounts from SQLite but can't decode them."
|
||||||
case .accountDAOFindBy: return "SQLite query failed when seaching for accounts in the database."
|
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 .rustLatestCachedBlockHeight: return .rustLatestCachedBlockHeight
|
||||||
case .rustScanProgressOutOfRange: return .rustScanProgressOutOfRange
|
case .rustScanProgressOutOfRange: return .rustScanProgressOutOfRange
|
||||||
case .rustGetWalletSummary: return .rustGetWalletSummary
|
case .rustGetWalletSummary: return .rustGetWalletSummary
|
||||||
|
case .rustProposeTransferFromURI: return .rustProposeTransferFromURI
|
||||||
case .accountDAOGetAll: return .accountDAOGetAll
|
case .accountDAOGetAll: return .accountDAOGetAll
|
||||||
case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode
|
case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode
|
||||||
case .accountDAOFindBy: return .accountDAOFindBy
|
case .accountDAOFindBy: return .accountDAOFindBy
|
||||||
|
|
|
@ -175,6 +175,8 @@ public enum ZcashErrorCode: String {
|
||||||
case rustScanProgressOutOfRange = "ZRUST0055"
|
case rustScanProgressOutOfRange = "ZRUST0055"
|
||||||
/// Error from rust layer when calling ZcashRustBackend.getWalletSummary
|
/// Error from rust layer when calling ZcashRustBackend.getWalletSummary
|
||||||
case rustGetWalletSummary = "ZRUST0056"
|
case rustGetWalletSummary = "ZRUST0056"
|
||||||
|
/// Error from rust layer when calling ZcashRustBackend.
|
||||||
|
case rustProposeTransferFromURI = "ZRUST0057"
|
||||||
/// SQLite query failed when fetching all accounts from the database.
|
/// SQLite query failed when fetching all accounts from the database.
|
||||||
case accountDAOGetAll = "ZADAO0001"
|
case accountDAOGetAll = "ZADAO0001"
|
||||||
/// Fetched accounts from SQLite but can't decode them.
|
/// Fetched accounts from SQLite but can't decode them.
|
||||||
|
|
|
@ -348,6 +348,10 @@ enum ZcashErrorDefinition {
|
||||||
/// - `rustError` contains error generated by the rust layer.
|
/// - `rustError` contains error generated by the rust layer.
|
||||||
// sourcery: code="ZRUST0056"
|
// sourcery: code="ZRUST0056"
|
||||||
case rustGetWalletSummary(_ rustError: String)
|
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
|
// MARK: - Account DAO
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
func decryptAndStoreTransaction(txBytes: [UInt8], minedHeight: Int32) async throws {
|
||||||
globalDBLock.lock()
|
globalDBLock.lock()
|
||||||
let result = zcashlc_decrypt_and_store_transaction(
|
let result = zcashlc_decrypt_and_store_transaction(
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// ZcashRustBackendWelding.swift
|
// ZcashRustBackendWelding.swift
|
||||||
// ZcashLightClientKit
|
// 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.
|
// Copyright © 2019 Electric Coin Company. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -208,6 +208,21 @@ protocol ZcashRustBackendWelding {
|
||||||
memo: MemoBytes?
|
memo: MemoBytes?
|
||||||
) async throws -> FfiProposal
|
) 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,
|
/// 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
|
/// that can then be authorized and made ready for submission to the network with
|
||||||
/// `createProposedTransaction`.
|
/// `createProposedTransaction`.
|
||||||
|
|
|
@ -215,7 +215,7 @@ public protocol Synchronizer: AnyObject {
|
||||||
/// - Parameter toAddress: the recipient's address.
|
/// - Parameter toAddress: the recipient's address.
|
||||||
/// - Parameter memo: an `Optional<Memo>`with the memo to include as part of the transaction. send `nil` when sending to transparent receivers otherwise the function will throw an error
|
/// - Parameter memo: an `Optional<Memo>`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`.
|
/// `SynchronizerErrors.notPrepared`.
|
||||||
@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.")
|
||||||
func sendToAddress(
|
func sendToAddress(
|
||||||
|
@ -225,12 +225,23 @@ public protocol Synchronizer: AnyObject {
|
||||||
memo: Memo?
|
memo: Memo?
|
||||||
) async throws -> ZcashTransaction.Overview
|
) 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`.
|
/// 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 spendingKey: the `UnifiedSpendingKey` that allows to spend transparent funds
|
||||||
/// - Parameter memo: the optional memo to include as part of the transaction.
|
/// - 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.
|
/// - 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`.
|
/// `SynchronizerErrors.notPrepared`.
|
||||||
@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.")
|
||||||
func shieldFunds(
|
func shieldFunds(
|
||||||
|
|
|
@ -80,6 +80,18 @@ extension CombineSDKSynchronizer: CombineSynchronizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func proposefulfillingPaymentURI(
|
||||||
|
_ uri: String,
|
||||||
|
accountIndex: Int
|
||||||
|
) -> SinglePublisher<Proposal, Error> {
|
||||||
|
AsyncToCombineGateway.executeThrowingAction() {
|
||||||
|
try await self.synchronizer.proposefulfillingPaymentURI(
|
||||||
|
uri,
|
||||||
|
accountIndex: accountIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func proposeShielding(
|
public func proposeShielding(
|
||||||
accountIndex: Int,
|
accountIndex: Int,
|
||||||
shieldingThreshold: Zatoshi,
|
shieldingThreshold: Zatoshi,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Combine
|
||||||
/// Synchronizer implementation for UIKit and iOS 13+
|
/// Synchronizer implementation for UIKit and iOS 13+
|
||||||
// swiftlint:disable type_body_length
|
// swiftlint:disable type_body_length
|
||||||
public class SDKSynchronizer: Synchronizer {
|
public class SDKSynchronizer: Synchronizer {
|
||||||
|
|
||||||
public var alias: ZcashSynchronizerAlias { initializer.alias }
|
public var alias: ZcashSynchronizerAlias { initializer.alias }
|
||||||
|
|
||||||
private lazy var streamsUpdateQueue = { DispatchQueue(label: "streamsUpdateQueue_\(initializer.alias.description)") }()
|
private lazy var streamsUpdateQueue = { DispatchQueue(label: "streamsUpdateQueue_\(initializer.alias.description)") }()
|
||||||
|
@ -290,14 +291,29 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
) async throws -> Proposal? {
|
) async throws -> Proposal? {
|
||||||
try throwIfUnprepared()
|
try throwIfUnprepared()
|
||||||
|
|
||||||
let proposal = try await transactionEncoder.proposeShielding(
|
return try await transactionEncoder.proposeShielding(
|
||||||
accountIndex: accountIndex,
|
accountIndex: accountIndex,
|
||||||
shieldingThreshold: shieldingThreshold,
|
shieldingThreshold: shieldingThreshold,
|
||||||
memoBytes: memo.asMemoBytes(),
|
memoBytes: memo.asMemoBytes(),
|
||||||
transparentReceiver: transparentReceiver?.stringEncoded
|
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(
|
public func createProposedTransactions(
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// TransactionEncoder.swift
|
// TransactionEncoder.swift
|
||||||
// ZcashLightClientKit
|
// ZcashLightClientKit
|
||||||
//
|
//
|
||||||
// Created by Francisco Gindre on 11/20/19.
|
// Created by Francisco Gindre on 2019-11-20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@ -72,6 +72,21 @@ protocol TransactionEncoder {
|
||||||
spendingKey: UnifiedSpendingKey
|
spendingKey: UnifiedSpendingKey
|
||||||
) async throws -> [ZcashTransaction.Overview]
|
) 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.
|
/// submits a transaction to the Zcash peer-to-peer network.
|
||||||
/// - Parameter transaction: a transaction overview
|
/// - Parameter transaction: a transaction overview
|
||||||
func submit(transaction: EncodedTransaction) async throws
|
func submit(transaction: EncodedTransaction) async throws
|
||||||
|
|
|
@ -87,6 +87,17 @@ class WalletTransactionEncoder: TransactionEncoder {
|
||||||
return Proposal(inner: proposal)
|
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(
|
func createProposedTransactions(
|
||||||
proposal: Proposal,
|
proposal: Proposal,
|
||||||
spendingKey: UnifiedSpendingKey
|
spendingKey: UnifiedSpendingKey
|
||||||
|
|
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
// MARK: - shieldFunds
|
||||||
|
|
||||||
var shieldFundsSpendingKeyMemoShieldingThresholdThrowableError: Error?
|
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
|
// MARK: - proposeShielding
|
||||||
|
|
||||||
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError: Error?
|
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError: Error?
|
||||||
|
|
Loading…
Reference in New Issue