Merge pull request #1382 from Electric-Coin-Company/1204-expose-proposals

Expose APIs for working with transaction proposals
This commit is contained in:
str4d 2024-03-08 14:50:06 +00:00 committed by GitHub
commit 7fcf1fad02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1189 additions and 257 deletions

View File

@ -4,6 +4,28 @@ All notable changes to this library will be documented in this file.
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
## Changed
- Migrated to `zcash-light-client-ffi 0.6.0`.
### [#1186] Enable ZIP 317 fees
- The SDK now generates transactions using [ZIP 317](https://zips.z.cash/zip-0317) fees,
instead of a fixed fee of 10,000 Zatoshi. Use `Proposal.totalFeeRequired` to check the
total fee for a transfer before creating it.
## Added
### [#1204] Expose APIs for working with transaction proposals
New `Synchronizer` APIs that enable constructing a proposal for transferring or
shielding funds, and then creating transactions from a proposal. The intermediate
proposal can be used to determine the required fee, before committing to producing
transactions.
The old `Synchronizer.sendToAddress` and `Synchronizer.shieldFunds` APIs have been
deprecated, and will be removed in 2.1.0 (which will create multiple transactions
at once for some recipients).
# 2.0.10 - 2024-02-12
## Added

View File

@ -176,8 +176,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "c90afd6cc092468e71810bc715ddb49be8210b75",
"version" : "0.5.1"
"revision" : "7c801be1f445402a433b32835a50d832e8a50437",
"version" : "0.6.0"
}
}
],

View File

@ -122,8 +122,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "c90afd6cc092468e71810bc715ddb49be8210b75",
"version" : "0.5.1"
"revision" : "7c801be1f445402a433b32835a50d832e8a50437",
"version" : "0.6.0"
}
}
],

View File

@ -16,7 +16,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.19.1"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"),
.package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", exact: "0.5.1")
.package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", exact: "0.6.0")
],
targets: [
.target(

View File

@ -36,6 +36,64 @@ public protocol ClosureSynchronizer {
func getUnifiedAddress(accountIndex: Int, completion: @escaping (Result<UnifiedAddress, Error>) -> Void)
func getTransparentAddress(accountIndex: Int, completion: @escaping (Result<TransparentAddress, Error>) -> Void)
/// Creates a proposal for transferring funds to the given recipient.
///
/// - Parameter accountIndex: the account from which to transfer funds.
/// - Parameter recipient: the recipient's address.
/// - Parameter amount: the amount to send in Zatoshi.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions. Use `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
/// `SynchronizerErrors.notPrepared`.
func proposeTransfer(
accountIndex: Int,
recipient: Recipient,
amount: Zatoshi,
memo: Memo?,
completion: @escaping (Result<Proposal, Error>) -> Void
)
/// Creates a proposal for shielding any transparent funds received by the given account.
///
/// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions.
/// - Parameter transparentReceiver: a specific transparent receiver within the account
/// that should be the source of transparent funds. Default is `nil` which
/// will select whichever of the account's transparent receivers has funds
/// to shield.
///
/// Returns the proposal, or `nil` if the transparent balance that would be shielded
/// is zero or below `shieldingThreshold`.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memo: Memo,
transparentReceiver: TransparentAddress?,
completion: @escaping (Result<Proposal?, Error>) -> Void
)
/// Creates the transactions in the given proposal.
///
/// - Parameter proposal: the proposal for which to create transactions.
/// - Parameter spendingKey: the `UnifiedSpendingKey` associated with the account for which the proposal was created.
///
/// Returns a stream of objects for the transactions that were created as part of the
/// proposal, indicating whether they were submitted to the network or if an error
/// occurred.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance
/// or since the last wipe then this method throws `SynchronizerErrors.notPrepared`.
func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey,
completion: @escaping (Result<AsyncThrowingStream<TransactionSubmitResult, Error>, Error>) -> Void
)
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
func sendToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
@ -44,6 +102,7 @@ public protocol ClosureSynchronizer {
completion: @escaping (Result<ZcashTransaction.Overview, Error>) -> Void
)
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,

View File

@ -35,6 +35,61 @@ public protocol CombineSynchronizer {
func getUnifiedAddress(accountIndex: Int) -> SinglePublisher<UnifiedAddress, Error>
func getTransparentAddress(accountIndex: Int) -> SinglePublisher<TransparentAddress, Error>
/// Creates a proposal for transferring funds to the given recipient.
///
/// - Parameter accountIndex: the account from which to transfer funds.
/// - Parameter recipient: the recipient's address.
/// - Parameter amount: the amount to send in Zatoshi.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions. Use `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
/// `SynchronizerErrors.notPrepared`.
func proposeTransfer(
accountIndex: Int,
recipient: Recipient,
amount: Zatoshi,
memo: Memo?
) -> SinglePublisher<Proposal, Error>
/// Creates a proposal for shielding any transparent funds received by the given account.
///
/// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions.
/// - Parameter transparentReceiver: a specific transparent receiver within the account
/// that should be the source of transparent funds. Default is `nil` which
/// will select whichever of the account's transparent receivers has funds
/// to shield.
///
/// Returns the proposal, or `nil` if the transparent balance that would be shielded
/// is zero or below `shieldingThreshold`.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memo: Memo,
transparentReceiver: TransparentAddress?
) -> SinglePublisher<Proposal?, Error>
/// Creates the transactions in the given proposal.
///
/// - Parameter proposal: the proposal for which to create transactions.
/// - Parameter spendingKey: the `UnifiedSpendingKey` associated with the account for which the proposal was created.
///
/// Returns a stream of objects for the transactions that were created as part of the
/// proposal, indicating whether they were submitted to the network or if an error
/// occurred.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance
/// or since the last wipe then this method throws `SynchronizerErrors.notPrepared`.
func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) -> SinglePublisher<AsyncThrowingStream<TransactionSubmitResult, Error>, Error>
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
func sendToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
@ -42,6 +97,7 @@ public protocol CombineSynchronizer {
memo: Memo?
) -> SinglePublisher<ZcashTransaction.Overview, Error>
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,

View File

@ -0,0 +1,45 @@
//
// Proposal.swift
//
//
// Created by Jack Grigg on 20/02/2024.
//
import Foundation
/// A data structure that describes a series of transactions to be created.
public struct Proposal: Equatable {
let inner: FfiProposal
/// Returns the number of transactions that this proposal will create.
///
/// This is equal to the number of `TransactionSubmitResult`s that will be returned
/// from `Synchronizer.createProposedTransactions`.
///
/// Proposals always create at least one transaction.
public func transactionCount() -> Int {
inner.steps.count
}
/// Returns the total fee to be paid across all proposed transactions, in zatoshis.
public func totalFeeRequired() -> Zatoshi {
inner.steps.reduce(Zatoshi.zero) { acc, step in
acc + Zatoshi(Int64(step.balance.feeRequired))
}
}
}
public extension Proposal {
/// IMPORTANT: This function is for testing purposes only. It produces fake invalid
/// data that can be used to check UI elements, but will always produce an error when
/// passed to `Synchronizer.createProposedTransactions`. It should never be called in
/// production code.
static func testOnlyFakeProposal(totalFee: UInt64) -> Self {
var ffiProposal = FfiProposal()
var balance = FfiTransactionBalance()
balance.feeRequired = totalFee
return Self(inner: ffiProposal)
}
}

View File

@ -143,18 +143,46 @@ extension FfiFeeRule: CaseIterable {
#endif // swift(>=4.2)
/// A data structure that describes the inputs to be consumed and outputs to
/// be produced in a proposed transaction.
/// A data structure that describes a series of transactions to be created.
struct FfiProposal {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// The version of this serialization format.
var protoVersion: UInt32 = 0
/// The fee rule used in constructing this proposal
var feeRule: FfiFeeRule = .notSpecified
/// The target height for which the proposal was constructed
///
/// The chain must contain at least this many blocks in order for the proposal to
/// be executed.
var minTargetHeight: UInt32 = 0
/// The series of transactions to be created.
var steps: [FfiProposalStep] = []
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
/// A data structure that describes the inputs to be consumed and outputs to
/// be produced in a proposed transaction.
struct FfiProposalStep {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// ZIP 321 serialized transaction request
var transactionRequest: String = String()
/// The vector of selected payment index / output pool mappings. Payment index
/// 0 corresponds to the payment with no explicit index.
var paymentOutputPools: [FfiPaymentOutputPool] = []
/// The anchor height to be used in creating the transaction, if any.
/// Setting the anchor height to zero will disallow the use of any shielded
/// inputs.
@ -174,16 +202,7 @@ struct FfiProposal {
/// Clears the value of `balance`. Subsequent reads from it will return its default value.
mutating func clearBalance() {self._balance = nil}
/// The fee rule used in constructing this proposal
var feeRule: FfiFeeRule = .notSpecified
/// The target height for which the proposal was constructed
///
/// The chain must contain at least this many blocks in order for the proposal to
/// be executed.
var minTargetHeight: UInt32 = 0
/// A flag indicating whether the proposal is for a shielding transaction,
/// A flag indicating whether the step is for a shielding transaction,
/// used for determining which OVK to select for wallet-internal outputs.
var isShielding: Bool = false
@ -194,8 +213,26 @@ struct FfiProposal {
fileprivate var _balance: FfiTransactionBalance? = nil
}
/// The unique identifier and value for each proposed input.
struct FfiProposedInput {
/// A mapping from ZIP 321 payment index to the output pool that has been chosen
/// for that payment, based upon the payment address and the selected inputs to
/// the transaction.
struct FfiPaymentOutputPool {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var paymentIndex: UInt32 = 0
var valuePool: FfiValuePool = .poolNotSpecified
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
/// The unique identifier and value for each proposed input that does not
/// require a back-reference to a prior step of the proposal.
struct FfiReceivedOutput {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -213,14 +250,113 @@ struct FfiProposedInput {
init() {}
}
/// A reference a payment in a prior step of the proposal. This payment must
/// belong to the wallet.
struct FfiPriorStepOutput {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var stepIndex: UInt32 = 0
var paymentIndex: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
/// A reference a change output from a prior step of the proposal.
struct FfiPriorStepChange {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var stepIndex: UInt32 = 0
var changeIndex: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
/// The unique identifier and value for an input to be used in the transaction.
struct FfiProposedInput {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var value: FfiProposedInput.OneOf_Value? = nil
var receivedOutput: FfiReceivedOutput {
get {
if case .receivedOutput(let v)? = value {return v}
return FfiReceivedOutput()
}
set {value = .receivedOutput(newValue)}
}
var priorStepOutput: FfiPriorStepOutput {
get {
if case .priorStepOutput(let v)? = value {return v}
return FfiPriorStepOutput()
}
set {value = .priorStepOutput(newValue)}
}
var priorStepChange: FfiPriorStepChange {
get {
if case .priorStepChange(let v)? = value {return v}
return FfiPriorStepChange()
}
set {value = .priorStepChange(newValue)}
}
var unknownFields = SwiftProtobuf.UnknownStorage()
enum OneOf_Value: Equatable {
case receivedOutput(FfiReceivedOutput)
case priorStepOutput(FfiPriorStepOutput)
case priorStepChange(FfiPriorStepChange)
#if !swift(>=4.1)
static func ==(lhs: FfiProposedInput.OneOf_Value, rhs: FfiProposedInput.OneOf_Value) -> Bool {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch (lhs, rhs) {
case (.receivedOutput, .receivedOutput): return {
guard case .receivedOutput(let l) = lhs, case .receivedOutput(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.priorStepOutput, .priorStepOutput): return {
guard case .priorStepOutput(let l) = lhs, case .priorStepOutput(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.priorStepChange, .priorStepChange): return {
guard case .priorStepChange(let l) = lhs, case .priorStepChange(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
#endif
}
init() {}
}
/// The proposed change outputs and fee value.
struct FfiTransactionBalance {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// A list of change output values.
var proposedChange: [FfiChangeValue] = []
/// The fee to be paid by the proposed transaction, in zatoshis.
var feeRequired: UInt64 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
@ -235,10 +371,14 @@ struct FfiChangeValue {
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// The value of a change output to be created, in zatoshis.
var value: UInt64 = 0
/// The value pool in which the change output should be created.
var valuePool: FfiValuePool = .poolNotSpecified
/// The optional memo that should be associated with the newly created change output.
/// Memos must not be present for transparent change outputs.
var memo: FfiMemoBytes {
get {return _memo ?? FfiMemoBytes()}
set {_memo = newValue}
@ -273,7 +413,13 @@ struct FfiMemoBytes {
extension FfiValuePool: @unchecked Sendable {}
extension FfiFeeRule: @unchecked Sendable {}
extension FfiProposal: @unchecked Sendable {}
extension FfiProposalStep: @unchecked Sendable {}
extension FfiPaymentOutputPool: @unchecked Sendable {}
extension FfiReceivedOutput: @unchecked Sendable {}
extension FfiPriorStepOutput: @unchecked Sendable {}
extension FfiPriorStepChange: @unchecked Sendable {}
extension FfiProposedInput: @unchecked Sendable {}
extension FfiProposedInput.OneOf_Value: @unchecked Sendable {}
extension FfiTransactionBalance: @unchecked Sendable {}
extension FfiChangeValue: @unchecked Sendable {}
extension FfiMemoBytes: @unchecked Sendable {}
@ -305,13 +451,9 @@ extension FfiProposal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
static let protoMessageName: String = _protobuf_package + ".Proposal"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "protoVersion"),
2: .same(proto: "transactionRequest"),
3: .same(proto: "anchorHeight"),
4: .same(proto: "inputs"),
5: .same(proto: "balance"),
6: .same(proto: "feeRule"),
7: .same(proto: "minTargetHeight"),
8: .same(proto: "isShielding"),
2: .same(proto: "feeRule"),
3: .same(proto: "minTargetHeight"),
4: .same(proto: "steps"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -321,13 +463,63 @@ extension FfiProposal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.protoVersion) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.transactionRequest) }()
case 2: try { try decoder.decodeSingularEnumField(value: &self.feeRule) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.minTargetHeight) }()
case 4: try { try decoder.decodeRepeatedMessageField(value: &self.steps) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.protoVersion != 0 {
try visitor.visitSingularUInt32Field(value: self.protoVersion, fieldNumber: 1)
}
if self.feeRule != .notSpecified {
try visitor.visitSingularEnumField(value: self.feeRule, fieldNumber: 2)
}
if self.minTargetHeight != 0 {
try visitor.visitSingularUInt32Field(value: self.minTargetHeight, fieldNumber: 3)
}
if !self.steps.isEmpty {
try visitor.visitRepeatedMessageField(value: self.steps, fieldNumber: 4)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: FfiProposal, rhs: FfiProposal) -> Bool {
if lhs.protoVersion != rhs.protoVersion {return false}
if lhs.feeRule != rhs.feeRule {return false}
if lhs.minTargetHeight != rhs.minTargetHeight {return false}
if lhs.steps != rhs.steps {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FfiProposalStep: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".ProposalStep"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "transactionRequest"),
2: .same(proto: "paymentOutputPools"),
3: .same(proto: "anchorHeight"),
4: .same(proto: "inputs"),
5: .same(proto: "balance"),
6: .same(proto: "isShielding"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.transactionRequest) }()
case 2: try { try decoder.decodeRepeatedMessageField(value: &self.paymentOutputPools) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.anchorHeight) }()
case 4: try { try decoder.decodeRepeatedMessageField(value: &self.inputs) }()
case 5: try { try decoder.decodeSingularMessageField(value: &self._balance) }()
case 6: try { try decoder.decodeSingularEnumField(value: &self.feeRule) }()
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.minTargetHeight) }()
case 8: try { try decoder.decodeSingularBoolField(value: &self.isShielding) }()
case 6: try { try decoder.decodeSingularBoolField(value: &self.isShielding) }()
default: break
}
}
@ -338,11 +530,11 @@ extension FfiProposal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if self.protoVersion != 0 {
try visitor.visitSingularUInt32Field(value: self.protoVersion, fieldNumber: 1)
}
if !self.transactionRequest.isEmpty {
try visitor.visitSingularStringField(value: self.transactionRequest, fieldNumber: 2)
try visitor.visitSingularStringField(value: self.transactionRequest, fieldNumber: 1)
}
if !self.paymentOutputPools.isEmpty {
try visitor.visitRepeatedMessageField(value: self.paymentOutputPools, fieldNumber: 2)
}
if self.anchorHeight != 0 {
try visitor.visitSingularUInt32Field(value: self.anchorHeight, fieldNumber: 3)
@ -353,34 +545,64 @@ extension FfiProposal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
try { if let v = self._balance {
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
} }()
if self.feeRule != .notSpecified {
try visitor.visitSingularEnumField(value: self.feeRule, fieldNumber: 6)
}
if self.minTargetHeight != 0 {
try visitor.visitSingularUInt32Field(value: self.minTargetHeight, fieldNumber: 7)
}
if self.isShielding != false {
try visitor.visitSingularBoolField(value: self.isShielding, fieldNumber: 8)
try visitor.visitSingularBoolField(value: self.isShielding, fieldNumber: 6)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: FfiProposal, rhs: FfiProposal) -> Bool {
if lhs.protoVersion != rhs.protoVersion {return false}
static func ==(lhs: FfiProposalStep, rhs: FfiProposalStep) -> Bool {
if lhs.transactionRequest != rhs.transactionRequest {return false}
if lhs.paymentOutputPools != rhs.paymentOutputPools {return false}
if lhs.anchorHeight != rhs.anchorHeight {return false}
if lhs.inputs != rhs.inputs {return false}
if lhs._balance != rhs._balance {return false}
if lhs.feeRule != rhs.feeRule {return false}
if lhs.minTargetHeight != rhs.minTargetHeight {return false}
if lhs.isShielding != rhs.isShielding {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FfiProposedInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".ProposedInput"
extension FfiPaymentOutputPool: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".PaymentOutputPool"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "paymentIndex"),
2: .same(proto: "valuePool"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.paymentIndex) }()
case 2: try { try decoder.decodeSingularEnumField(value: &self.valuePool) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.paymentIndex != 0 {
try visitor.visitSingularUInt32Field(value: self.paymentIndex, fieldNumber: 1)
}
if self.valuePool != .poolNotSpecified {
try visitor.visitSingularEnumField(value: self.valuePool, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: FfiPaymentOutputPool, rhs: FfiPaymentOutputPool) -> Bool {
if lhs.paymentIndex != rhs.paymentIndex {return false}
if lhs.valuePool != rhs.valuePool {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FfiReceivedOutput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".ReceivedOutput"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "txid"),
2: .same(proto: "valuePool"),
@ -419,7 +641,7 @@ extension FfiProposedInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: FfiProposedInput, rhs: FfiProposedInput) -> Bool {
static func ==(lhs: FfiReceivedOutput, rhs: FfiReceivedOutput) -> Bool {
if lhs.txid != rhs.txid {return false}
if lhs.valuePool != rhs.valuePool {return false}
if lhs.index != rhs.index {return false}
@ -429,6 +651,170 @@ extension FfiProposedInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
}
}
extension FfiPriorStepOutput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".PriorStepOutput"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "stepIndex"),
2: .same(proto: "paymentIndex"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.stepIndex) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.paymentIndex) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.stepIndex != 0 {
try visitor.visitSingularUInt32Field(value: self.stepIndex, fieldNumber: 1)
}
if self.paymentIndex != 0 {
try visitor.visitSingularUInt32Field(value: self.paymentIndex, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: FfiPriorStepOutput, rhs: FfiPriorStepOutput) -> Bool {
if lhs.stepIndex != rhs.stepIndex {return false}
if lhs.paymentIndex != rhs.paymentIndex {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FfiPriorStepChange: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".PriorStepChange"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "stepIndex"),
2: .same(proto: "changeIndex"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.stepIndex) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.changeIndex) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.stepIndex != 0 {
try visitor.visitSingularUInt32Field(value: self.stepIndex, fieldNumber: 1)
}
if self.changeIndex != 0 {
try visitor.visitSingularUInt32Field(value: self.changeIndex, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: FfiPriorStepChange, rhs: FfiPriorStepChange) -> Bool {
if lhs.stepIndex != rhs.stepIndex {return false}
if lhs.changeIndex != rhs.changeIndex {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FfiProposedInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".ProposedInput"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "receivedOutput"),
2: .same(proto: "priorStepOutput"),
3: .same(proto: "priorStepChange"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try {
var v: FfiReceivedOutput?
var hadOneofValue = false
if let current = self.value {
hadOneofValue = true
if case .receivedOutput(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.value = .receivedOutput(v)
}
}()
case 2: try {
var v: FfiPriorStepOutput?
var hadOneofValue = false
if let current = self.value {
hadOneofValue = true
if case .priorStepOutput(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.value = .priorStepOutput(v)
}
}()
case 3: try {
var v: FfiPriorStepChange?
var hadOneofValue = false
if let current = self.value {
hadOneofValue = true
if case .priorStepChange(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.value = .priorStepChange(v)
}
}()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
switch self.value {
case .receivedOutput?: try {
guard case .receivedOutput(let v)? = self.value else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
}()
case .priorStepOutput?: try {
guard case .priorStepOutput(let v)? = self.value else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
}()
case .priorStepChange?: try {
guard case .priorStepChange(let v)? = self.value else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: FfiProposedInput, rhs: FfiProposedInput) -> Bool {
if lhs.value != rhs.value {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FfiTransactionBalance: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".TransactionBalance"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View File

@ -6,12 +6,29 @@ syntax = "proto3";
package cash.z.wallet.sdk.ffi;
option swift_prefix = "Ffi";
// A data structure that describes a series of transactions to be created.
message Proposal {
// The version of this serialization format.
uint32 protoVersion = 1;
// The fee rule used in constructing this proposal
FeeRule feeRule = 2;
// The target height for which the proposal was constructed
//
// The chain must contain at least this many blocks in order for the proposal to
// be executed.
uint32 minTargetHeight = 3;
// The series of transactions to be created.
repeated ProposalStep steps = 4;
}
// A data structure that describes the inputs to be consumed and outputs to
// be produced in a proposed transaction.
message Proposal {
uint32 protoVersion = 1;
message ProposalStep {
// ZIP 321 serialized transaction request
string transactionRequest = 2;
string transactionRequest = 1;
// The vector of selected payment index / output pool mappings. Payment index
// 0 corresponds to the payment with no explicit index.
repeated PaymentOutputPool paymentOutputPools = 2;
// The anchor height to be used in creating the transaction, if any.
// Setting the anchor height to zero will disallow the use of any shielded
// inputs.
@ -21,16 +38,9 @@ message Proposal {
// The total value, fee value, and change outputs of the proposed
// transaction
TransactionBalance balance = 5;
// The fee rule used in constructing this proposal
FeeRule feeRule = 6;
// The target height for which the proposal was constructed
//
// The chain must contain at least this many blocks in order for the proposal to
// be executed.
uint32 minTargetHeight = 7;
// A flag indicating whether the proposal is for a shielding transaction,
// A flag indicating whether the step is for a shielding transaction,
// used for determining which OVK to select for wallet-internal outputs.
bool isShielding = 8;
bool isShielding = 6;
}
enum ValuePool {
@ -47,14 +57,45 @@ enum ValuePool {
Orchard = 3;
}
// The unique identifier and value for each proposed input.
message ProposedInput {
// A mapping from ZIP 321 payment index to the output pool that has been chosen
// for that payment, based upon the payment address and the selected inputs to
// the transaction.
message PaymentOutputPool {
uint32 paymentIndex = 1;
ValuePool valuePool = 2;
}
// The unique identifier and value for each proposed input that does not
// require a back-reference to a prior step of the proposal.
message ReceivedOutput {
bytes txid = 1;
ValuePool valuePool = 2;
uint32 index = 3;
uint64 value = 4;
}
// A reference a payment in a prior step of the proposal. This payment must
// belong to the wallet.
message PriorStepOutput {
uint32 stepIndex = 1;
uint32 paymentIndex = 2;
}
// A reference a change output from a prior step of the proposal.
message PriorStepChange {
uint32 stepIndex = 1;
uint32 changeIndex = 2;
}
// The unique identifier and value for an input to be used in the transaction.
message ProposedInput {
oneof value {
ReceivedOutput receivedOutput = 1;
PriorStepOutput priorStepOutput = 2;
PriorStepChange priorStepChange = 3;
}
}
// The fee rule used in constructing a Proposal
enum FeeRule {
// Protobuf requires that enums have a zero discriminant as the default
@ -72,15 +113,21 @@ enum FeeRule {
// The proposed change outputs and fee value.
message TransactionBalance {
// A list of change output values.
repeated ChangeValue proposedChange = 1;
// The fee to be paid by the proposed transaction, in zatoshis.
uint64 feeRequired = 2;
}
// A proposed change output. If the transparent value pool is selected,
// the `memo` field must be null.
message ChangeValue {
// The value of a change output to be created, in zatoshis.
uint64 value = 1;
// The value pool in which the change output should be created.
ValuePool valuePool = 2;
// The optional memo that should be associated with the newly created change output.
// Memos must not be present for transparent change outputs.
MemoBytes memo = 3;
}

View File

@ -13,7 +13,7 @@ let globalDBLock = NSLock()
actor ZcashRustBackend: ZcashRustBackendWelding {
let minimumConfirmations: UInt32 = 10
let useZIP317Fees = false
let useZIP317Fees = true
let dbData: (String, UInt)
let fsBlockDbRoot: (String, UInt)
@ -619,8 +619,9 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
func proposeShielding(
account: Int32,
memo: MemoBytes?,
shieldingThreshold: Zatoshi
) async throws -> FfiProposal {
shieldingThreshold: Zatoshi,
transparentReceiver: String?
) async throws -> FfiProposal? {
globalDBLock.lock()
let proposal = zcashlc_propose_shielding(
dbData.0,
@ -628,6 +629,7 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
account,
memo?.bytes,
UInt64(shieldingThreshold.amount),
transparentReceiver.map { [CChar]($0.utf8CString) },
networkType.networkId,
minimumConfirmations,
useZIP317Fees
@ -646,44 +648,46 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
))
}
func createProposedTransaction(
func createProposedTransactions(
proposal: FfiProposal,
usk: UnifiedSpendingKey
) async throws -> Data {
var contiguousTxIdBytes = ContiguousArray<UInt8>([UInt8](repeating: 0x0, count: 32))
) async throws -> [Data] {
let proposalBytes = try proposal.serializedData(partial: false).bytes
globalDBLock.lock()
let success = contiguousTxIdBytes.withUnsafeMutableBufferPointer { txIdBytePtr in
proposalBytes.withUnsafeBufferPointer { proposalPtr in
usk.bytes.withUnsafeBufferPointer { uskPtr in
zcashlc_create_proposed_transaction(
dbData.0,
dbData.1,
proposalPtr.baseAddress,
UInt(proposalBytes.count),
uskPtr.baseAddress,
UInt(usk.bytes.count),
spendParamsPath.0,
spendParamsPath.1,
outputParamsPath.0,
outputParamsPath.1,
networkType.networkId,
txIdBytePtr.baseAddress
)
}
let txIdsPtr = proposalBytes.withUnsafeBufferPointer { proposalPtr in
usk.bytes.withUnsafeBufferPointer { uskPtr in
zcashlc_create_proposed_transactions(
dbData.0,
dbData.1,
proposalPtr.baseAddress,
UInt(proposalBytes.count),
uskPtr.baseAddress,
UInt(usk.bytes.count),
spendParamsPath.0,
spendParamsPath.1,
outputParamsPath.0,
outputParamsPath.1,
networkType.networkId
)
}
}
globalDBLock.unlock()
guard success else {
guard let txIdsPtr else {
throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`createToAddress` failed with unknown error"))
}
return contiguousTxIdBytes.withUnsafeBufferPointer { txIdBytePtr in
Data(txIdBytePtr)
defer { zcashlc_free_txids(txIdsPtr) }
var txIds: [Data] = []
for i in (0 ..< Int(txIdsPtr.pointee.len)) {
let txId = FfiTxId(tuple: txIdsPtr.pointee.ptr.advanced(by: i).pointee)
txIds.append(Data(txId.array))
}
return txIds
}
nonisolated func consensusBranchIdFor(height: Int32) throws -> Int32 {
@ -823,3 +827,12 @@ extension FfiScanProgress {
)
}
}
struct FfiTxId {
var tuple: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
var array: [UInt8] {
withUnsafeBytes(of: self.tuple) { buf in
[UInt8](buf)
}
}
}

View File

@ -212,23 +212,31 @@ protocol ZcashRustBackendWelding {
/// that can then be authorized and made ready for submission to the network with
/// `createProposedTransaction`.
///
/// Returns the proposal, or `nil` if the transparent balance that would be shielded
/// is zero or below `shieldingThreshold`.
///
/// - parameter account: index of the given account
/// - Parameter memo: the `Memo` for this transaction
/// - Parameter transparentReceiver: a specific transparent receiver within the account
/// that should be the source of transparent funds. Default is `nil` which
/// will select whichever of the account's transparent receivers has funds
/// to shield.
/// - Throws: `rustShieldFunds` if rust layer returns error.
func proposeShielding(
account: Int32,
memo: MemoBytes?,
shieldingThreshold: Zatoshi
) async throws -> FfiProposal
shieldingThreshold: Zatoshi,
transparentReceiver: String?
) async throws -> FfiProposal?
/// Creates a transaction from the given proposal.
/// - Parameter proposal: the transaction proposal.
/// - Parameter usk: `UnifiedSpendingKey` for the account that controls the funds to be spent.
/// - Throws: `rustCreateToAddress`.
func createProposedTransaction(
func createProposedTransactions(
proposal: FfiProposal,
usk: UnifiedSpendingKey
) async throws -> Data
) async throws -> [Data]
/// Gets the consensus branch id for the given height
/// - Parameter height: the height you what to know the branch id for

View File

@ -154,15 +154,70 @@ public protocol Synchronizer: AnyObject {
/// - Parameter accountIndex: the optional accountId whose address is of interest. By default, the first account is used.
/// - Returns the address or nil if account index is incorrect
func getTransparentAddress(accountIndex: Int) async throws -> TransparentAddress
/// Creates a proposal for transferring funds to the given recipient.
///
/// - Parameter accountIndex: the account from which to transfer funds.
/// - Parameter recipient: the recipient's address.
/// - Parameter amount: the amount to send in Zatoshi.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions. Use `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
/// `SynchronizerErrors.notPrepared`.
func proposeTransfer(
accountIndex: Int,
recipient: Recipient,
amount: Zatoshi,
memo: Memo?
) async throws -> Proposal
/// Creates a proposal for shielding any transparent funds received by the given account.
///
/// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions.
/// - Parameter transparentReceiver: a specific transparent receiver within the account
/// that should be the source of transparent funds. Default is `nil` which
/// will select whichever of the account's transparent receivers has funds
/// to shield.
///
/// Returns the proposal, or `nil` if the transparent balance that would be shielded
/// is zero or below `shieldingThreshold`.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memo: Memo,
transparentReceiver: TransparentAddress?
) async throws -> Proposal?
/// Creates the transactions in the given proposal.
///
/// - Parameter proposal: the proposal for which to create transactions.
/// - Parameter spendingKey: the `UnifiedSpendingKey` associated with the account for which the proposal was created.
///
/// Returns a stream of objects for the transactions that were created as part of the
/// proposal, indicating whether they were submitted to the network or if an error
/// occurred.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance
/// or since the last wipe then this method throws `SynchronizerErrors.notPrepared`.
func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) async throws -> AsyncThrowingStream<TransactionSubmitResult, Error>
/// Sends zatoshi.
/// - Parameter spendingKey: the `UnifiedSpendingKey` that allows spends to occur.
/// - Parameter zatoshi: the amount to send in Zatoshi.
/// - 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
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// If `prepare()` hasn't already been called since creation of the 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(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
@ -173,9 +228,11 @@ public protocol Synchronizer: AnyObject {
/// 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 creating of synchronizer instance or since the last wipe then this method throws
/// If `prepare()` hasn't already been called since creation of the 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(
spendingKey: UnifiedSpendingKey,
memo: Memo,
@ -227,7 +284,7 @@ public protocol Synchronizer: AnyObject {
/// Returns the latests UTXOs for the given address from the specified height on
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) async throws -> RefreshedUTXOs
@ -251,7 +308,7 @@ public protocol Synchronizer: AnyObject {
/// `rewind(policy:)` itself doesn't start the sync process when it's done and it doesn't trigger notifications as regorg would. After it is done
/// you have start the sync process by calling `start()`
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then returned publisher emits
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then returned publisher emits
/// `SynchronizerErrors.notPrepared` error.
///
/// - Parameter policy: the rewind policy
@ -433,6 +490,19 @@ public enum RewindPolicy {
case quick
}
/// The result of submitting a transaction to the network.
///
/// - success: the transaction was successfully submitted to the mempool.
/// - grpcFailure: the transaction failed to reach the lightwalletd server.
/// - submitFailure: the transaction reached the lightwalletd server but failed to enter the mempool.
/// - notAttempted: the transaction was created and is in the local wallet, but was not submitted to the network.
public enum TransactionSubmitResult: Equatable {
case success(txId: Data)
case grpcFailure(txId: Data, error: LightWalletServiceError)
case submitFailure(txId: Data, code: Int, description: String)
case notAttempted(txId: Data)
}
extension InternalSyncStatus {
public static func == (lhs: InternalSyncStatus, rhs: InternalSyncStatus) -> Bool {
switch (lhs, rhs) {

View File

@ -70,6 +70,46 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer {
}
}
public func proposeTransfer(
accountIndex: Int,
recipient: Recipient,
amount: Zatoshi,
memo: Memo?,
completion: @escaping (Result<Proposal, Error>) -> Void
) {
AsyncToClosureGateway.executeThrowingAction(completion) {
try await self.synchronizer.proposeTransfer(accountIndex: accountIndex, recipient: recipient, amount: amount, memo: memo)
}
}
public func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memo: Memo,
transparentReceiver: TransparentAddress? = nil,
completion: @escaping (Result<Proposal?, Error>) -> Void
) {
AsyncToClosureGateway.executeThrowingAction(completion) {
try await self.synchronizer.proposeShielding(
accountIndex: accountIndex,
shieldingThreshold: shieldingThreshold,
memo: memo,
transparentReceiver: transparentReceiver
)
}
}
public func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey,
completion: @escaping (Result<AsyncThrowingStream<TransactionSubmitResult, Error>, Error>) -> Void
) {
AsyncToClosureGateway.executeThrowingAction(completion) {
try await self.synchronizer.createProposedTransactions(proposal: proposal, spendingKey: spendingKey)
}
}
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
public func sendToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
@ -82,6 +122,7 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer {
}
}
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
public func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,

View File

@ -69,6 +69,43 @@ extension CombineSDKSynchronizer: CombineSynchronizer {
}
}
public func proposeTransfer(
accountIndex: Int,
recipient: Recipient,
amount: Zatoshi,
memo: Memo?
) -> SinglePublisher<Proposal, Error> {
AsyncToCombineGateway.executeThrowingAction() {
try await self.synchronizer.proposeTransfer(accountIndex: accountIndex, recipient: recipient, amount: amount, memo: memo)
}
}
public func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memo: Memo,
transparentReceiver: TransparentAddress? = nil
) -> SinglePublisher<Proposal?, Error> {
AsyncToCombineGateway.executeThrowingAction() {
try await self.synchronizer.proposeShielding(
accountIndex: accountIndex,
shieldingThreshold: shieldingThreshold,
memo: memo,
transparentReceiver: transparentReceiver
)
}
}
public func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) -> SinglePublisher<AsyncThrowingStream<TransactionSubmitResult, Error>, Error> {
AsyncToCombineGateway.executeThrowingAction() {
try await self.synchronizer.createProposedTransactions(proposal: proposal, spendingKey: spendingKey)
}
}
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
public func sendToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
@ -80,6 +117,7 @@ extension CombineSDKSynchronizer: CombineSynchronizer {
}
}
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
public func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo,

View File

@ -265,6 +265,84 @@ public class SDKSynchronizer: Synchronizer {
// MARK: Synchronizer methods
public func proposeTransfer(accountIndex: Int, recipient: Recipient, amount: Zatoshi, memo: Memo?) async throws -> Proposal {
try throwIfUnprepared()
if case Recipient.transparent = recipient, memo != nil {
throw ZcashError.synchronizerSendMemoToTransparentAddress
}
let proposal = try await transactionEncoder.proposeTransfer(
accountIndex: accountIndex,
recipient: recipient.stringEncoded,
amount: amount,
memoBytes: memo?.asMemoBytes()
)
return proposal
}
public func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memo: Memo,
transparentReceiver: TransparentAddress? = nil
) async throws -> Proposal? {
try throwIfUnprepared()
let proposal = try await transactionEncoder.proposeShielding(
accountIndex: accountIndex,
shieldingThreshold: shieldingThreshold,
memoBytes: memo.asMemoBytes(),
transparentReceiver: transparentReceiver?.stringEncoded
)
return proposal
}
public func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) async throws -> AsyncThrowingStream<TransactionSubmitResult, Error> {
try throwIfUnprepared()
try await SaplingParameterDownloader.downloadParamsIfnotPresent(
spendURL: initializer.spendParamsURL,
spendSourceURL: initializer.saplingParamsSourceURL.spendParamFileURL,
outputURL: initializer.outputParamsURL,
outputSourceURL: initializer.saplingParamsSourceURL.outputParamFileURL,
logger: logger
)
let transactions = try await transactionEncoder.createProposedTransactions(
proposal: proposal,
spendingKey: spendingKey
)
var iterator = transactions.makeIterator()
var submitFailed = false
return AsyncThrowingStream() {
guard let transaction = iterator.next() else { return nil }
if submitFailed {
return .notAttempted(txId: transaction.rawID)
} else {
let encodedTransaction = try transaction.encodedTransaction()
do {
try await self.transactionEncoder.submit(transaction: encodedTransaction)
return TransactionSubmitResult.success(txId: transaction.rawID)
} catch ZcashError.serviceSubmitFailed(let error) {
submitFailed = true
return TransactionSubmitResult.grpcFailure(txId: transaction.rawID, error: error)
} catch TransactionEncoderError.submitError(let code, let message) {
submitFailed = true
return TransactionSubmitResult.submitFailure(txId: transaction.rawID, code: code, description: message)
}
}
}
}
public func sendToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
@ -312,13 +390,21 @@ public class SDKSynchronizer: Synchronizer {
throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds
}
let transaction = try await transactionEncoder.createShieldingTransaction(
spendingKey: spendingKey,
guard let proposal = try await transactionEncoder.proposeShielding(
accountIndex: Int(spendingKey.account),
shieldingThreshold: shieldingThreshold,
memoBytes: memo.asMemoBytes(),
from: Int(spendingKey.account)
transparentReceiver: nil
) else { throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds }
let transactions = try await transactionEncoder.createProposedTransactions(
proposal: proposal,
spendingKey: spendingKey
)
assert(transactions.count == 1, "Rust backend doesn't produce multiple transactions yet")
let transaction = transactions[0]
let encodedTx = try transaction.encodedTransaction()
try await transactionEncoder.submit(transaction: encodedTx)
@ -339,14 +425,21 @@ public class SDKSynchronizer: Synchronizer {
throw ZcashError.synchronizerSendMemoToTransparentAddress
}
let transaction = try await transactionEncoder.createTransaction(
spendingKey: spendingKey,
zatoshi: zatoshi,
to: recipient.stringEncoded,
memoBytes: memo?.asMemoBytes(),
from: Int(spendingKey.account)
let proposal = try await transactionEncoder.proposeTransfer(
accountIndex: Int(spendingKey.account),
recipient: recipient.stringEncoded,
amount: zatoshi,
memoBytes: memo?.asMemoBytes()
)
let transactions = try await transactionEncoder.createProposedTransactions(
proposal: proposal,
spendingKey: spendingKey
)
assert(transactions.count == 1, "Rust backend doesn't produce multiple transactions yet")
let transaction = transactions[0]
let encodedTransaction = try transaction.encodedTransaction()
try await transactionEncoder.submit(transaction: encodedTransaction)

View File

@ -19,44 +19,58 @@ public enum TransactionEncoderError: Error {
}
protocol TransactionEncoder {
/// Creates a transaction, 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).
/// Creates a proposal for transferring funds to the given recipient.
///
/// - Parameters:
/// - Parameter spendingKey: a `UnifiedSpendingKey` containing the spending key
/// - Parameter zatoshi: the amount to send in `Zatoshi`
/// - Parameter to: string containing the recipient address
/// - Parameter MemoBytes: string containing the memo (optional)
/// - Parameter accountIndex: index of the account that will be used to send the funds
/// - Throws:
/// - `walletTransEncoderCreateTransactionMissingSaplingParams` if the sapling parameters aren't downloaded.
/// - Some `ZcashError.rust*` if the creation of transaction fails.
func createTransaction(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to address: String,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> ZcashTransaction.Overview
/// Creates a transaction that will attempt to shield transparent funds that are present on the blocks cache .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).
/// - Parameter accountIndex: the account from which to transfer funds.
/// - Parameter recipient: string containing the recipient's address.
/// - Parameter amount: the amount to send in Zatoshi.
/// - Parameter memoBytes: an optional memo to include as part of the proposal's transactions. Use `nil` when sending to transparent receivers otherwise the function will throw an error.
///
/// - Parameters:
/// - Parameter spendingKey: `UnifiedSpendingKey` to spend the UTXOs
/// - Parameter memoBytes: containing the memo (optional)
/// - Parameter accountIndex: index of the account that will be used to send the funds
/// - Throws:
/// - `walletTransEncoderShieldFundsMissingSaplingParams` if the sapling parameters aren't downloaded.
/// - Some `ZcashError.rust*` if the creation of transaction fails.
func createShieldingTransaction(
spendingKey: UnifiedSpendingKey,
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeTransfer(
accountIndex: Int,
recipient: String,
amount: Zatoshi,
memoBytes: MemoBytes?
) async throws -> Proposal
/// Creates a proposal for shielding any transparent funds received by the given account.
///
/// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created.
/// - Parameter memoBytes: an optional memo to include as part of the proposal's transactions.
/// - Parameter transparentReceiver: a specific transparent receiver within the account
/// that should be the source of transparent funds. Default is `nil` which
/// will select whichever of the account's transparent receivers has funds
/// to shield.
///
/// Returns the proposal, or `nil` if the transparent balance that would be shielded
/// is zero or below `shieldingThreshold`.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> ZcashTransaction.Overview
transparentReceiver: String?
) async throws -> Proposal?
/// Creates the transactions in the given proposal.
///
/// - Parameter proposal: the proposal for which to create transactions.
/// - Parameter spendingKey: the `UnifiedSpendingKey` associated with the account for which the proposal was created.
/// - Throws:
/// - `walletTransEncoderCreateTransactionMissingSaplingParams` if the sapling parameters aren't downloaded.
/// - Some `ZcashError.rust*` if the creation of transaction(s) fails.
///
/// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) async throws -> [ZcashTransaction.Overview]
/// submits a transaction to the Zcash peer-to-peer network.
/// - Parameter transaction: a transaction overview

View File

@ -54,93 +54,61 @@ class WalletTransactionEncoder: TransactionEncoder {
logger: initializer.logger
)
}
func createTransaction(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to address: String,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> ZcashTransaction.Overview {
let txId = try await createSpend(
spendingKey: spendingKey,
zatoshi: zatoshi,
to: address,
memoBytes: memoBytes,
from: accountIndex
func proposeTransfer(
accountIndex: Int,
recipient: String,
amount: Zatoshi,
memoBytes: MemoBytes?
) async throws -> Proposal {
let proposal = try await rustBackend.proposeTransfer(
account: Int32(accountIndex),
to: recipient,
value: amount.amount,
memo: memoBytes
)
logger.debug("transaction id: \(txId)")
return try await repository.find(rawID: txId)
return Proposal(inner: proposal)
}
func createSpend(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to address: String,
func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> Data {
transparentReceiver: String? = nil
) async throws -> Proposal? {
guard let proposal = try await rustBackend.proposeShielding(
account: Int32(accountIndex),
memo: memoBytes,
shieldingThreshold: shieldingThreshold,
transparentReceiver: transparentReceiver
) else { return nil }
return Proposal(inner: proposal)
}
func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) async throws -> [ZcashTransaction.Overview] {
guard ensureParams(spend: self.spendParamsURL, output: self.outputParamsURL) else {
throw ZcashError.walletTransEncoderCreateTransactionMissingSaplingParams
}
// TODO: Expose the proposal in a way that enables querying its fee.
let proposal = try await rustBackend.proposeTransfer(
account: Int32(spendingKey.account),
to: address,
value: zatoshi.amount,
memo: memoBytes
)
let txId = try await rustBackend.createProposedTransaction(
proposal: proposal,
let txIds = try await rustBackend.createProposedTransactions(
proposal: proposal.inner,
usk: spendingKey
)
return txId
}
func createShieldingTransaction(
spendingKey: UnifiedSpendingKey,
shieldingThreshold: Zatoshi,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> ZcashTransaction.Overview {
let txId = try await createShieldingSpend(
spendingKey: spendingKey,
shieldingThreshold: shieldingThreshold,
memo: memoBytes,
accountIndex: accountIndex
)
logger.debug("transaction id: \(txId)")
return try await repository.find(rawID: txId)
}
logger.debug("transaction ids: \(txIds)")
func createShieldingSpend(
spendingKey: UnifiedSpendingKey,
shieldingThreshold: Zatoshi,
memo: MemoBytes?,
accountIndex: Int
) async throws -> Data {
guard ensureParams(spend: self.spendParamsURL, output: self.outputParamsURL) else {
throw ZcashError.walletTransEncoderShieldFundsMissingSaplingParams
var txs: [ZcashTransaction.Overview] = []
for txId in txIds {
txs.append(try await repository.find(rawID: txId))
}
// TODO: Expose the proposal in a way that enables querying its fee.
let proposal = try await rustBackend.proposeShielding(
account: Int32(spendingKey.account),
memo: memo,
shieldingThreshold: shieldingThreshold
)
let txId = try await rustBackend.createProposedTransaction(
proposal: proposal,
usk: spendingKey
)
return txId
return txs
}
func submit(

View File

@ -1397,6 +1397,78 @@ class SynchronizerMock: Synchronizer {
}
}
// MARK: - proposeTransfer
var proposeTransferAccountIndexRecipientAmountMemoThrowableError: Error?
var proposeTransferAccountIndexRecipientAmountMemoCallsCount = 0
var proposeTransferAccountIndexRecipientAmountMemoCalled: Bool {
return proposeTransferAccountIndexRecipientAmountMemoCallsCount > 0
}
var proposeTransferAccountIndexRecipientAmountMemoReceivedArguments: (accountIndex: Int, recipient: Recipient, amount: Zatoshi, memo: Memo?)?
var proposeTransferAccountIndexRecipientAmountMemoReturnValue: Proposal!
var proposeTransferAccountIndexRecipientAmountMemoClosure: ((Int, Recipient, Zatoshi, Memo?) async throws -> Proposal)?
func proposeTransfer(accountIndex: Int, recipient: Recipient, amount: Zatoshi, memo: Memo?) async throws -> Proposal {
if let error = proposeTransferAccountIndexRecipientAmountMemoThrowableError {
throw error
}
proposeTransferAccountIndexRecipientAmountMemoCallsCount += 1
proposeTransferAccountIndexRecipientAmountMemoReceivedArguments = (accountIndex: accountIndex, recipient: recipient, amount: amount, memo: memo)
if let closure = proposeTransferAccountIndexRecipientAmountMemoClosure {
return try await closure(accountIndex, recipient, amount, memo)
} else {
return proposeTransferAccountIndexRecipientAmountMemoReturnValue
}
}
// MARK: - proposeShielding
var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverThrowableError: Error?
var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount = 0
var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCalled: Bool {
return proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount > 0
}
var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReceivedArguments: (accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo, transparentReceiver: TransparentAddress?)?
var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReturnValue: Proposal?
var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverClosure: ((Int, Zatoshi, Memo, TransparentAddress?) async throws -> Proposal?)?
func proposeShielding(accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo, transparentReceiver: TransparentAddress?) async throws -> Proposal? {
if let error = proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverThrowableError {
throw error
}
proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount += 1
proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReceivedArguments = (accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memo: memo, transparentReceiver: transparentReceiver)
if let closure = proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverClosure {
return try await closure(accountIndex, shieldingThreshold, memo, transparentReceiver)
} else {
return proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReturnValue
}
}
// MARK: - createProposedTransactions
var createProposedTransactionsProposalSpendingKeyThrowableError: Error?
var createProposedTransactionsProposalSpendingKeyCallsCount = 0
var createProposedTransactionsProposalSpendingKeyCalled: Bool {
return createProposedTransactionsProposalSpendingKeyCallsCount > 0
}
var createProposedTransactionsProposalSpendingKeyReceivedArguments: (proposal: Proposal, spendingKey: UnifiedSpendingKey)?
var createProposedTransactionsProposalSpendingKeyReturnValue: AsyncThrowingStream<TransactionSubmitResult, Error>!
var createProposedTransactionsProposalSpendingKeyClosure: ((Proposal, UnifiedSpendingKey) async throws -> AsyncThrowingStream<TransactionSubmitResult, Error>)?
func createProposedTransactions(proposal: Proposal, spendingKey: UnifiedSpendingKey) async throws -> AsyncThrowingStream<TransactionSubmitResult, Error> {
if let error = createProposedTransactionsProposalSpendingKeyThrowableError {
throw error
}
createProposedTransactionsProposalSpendingKeyCallsCount += 1
createProposedTransactionsProposalSpendingKeyReceivedArguments = (proposal: proposal, spendingKey: spendingKey)
if let closure = createProposedTransactionsProposalSpendingKeyClosure {
return try await closure(proposal, spendingKey)
} else {
return createProposedTransactionsProposalSpendingKeyReturnValue
}
}
// MARK: - sendToAddress
var sendToAddressSpendingKeyZatoshiToAddressMemoThrowableError: Error?
@ -2756,67 +2828,67 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding {
// MARK: - proposeShielding
var proposeShieldingAccountMemoShieldingThresholdThrowableError: Error?
func setProposeShieldingAccountMemoShieldingThresholdThrowableError(_ param: Error?) async {
proposeShieldingAccountMemoShieldingThresholdThrowableError = param
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError: Error?
func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError(_ param: Error?) async {
proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError = param
}
var proposeShieldingAccountMemoShieldingThresholdCallsCount = 0
var proposeShieldingAccountMemoShieldingThresholdCalled: Bool {
return proposeShieldingAccountMemoShieldingThresholdCallsCount > 0
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount = 0
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCalled: Bool {
return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount > 0
}
var proposeShieldingAccountMemoShieldingThresholdReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi)?
var proposeShieldingAccountMemoShieldingThresholdReturnValue: FfiProposal!
func setProposeShieldingAccountMemoShieldingThresholdReturnValue(_ param: FfiProposal) async {
proposeShieldingAccountMemoShieldingThresholdReturnValue = param
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?)?
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue: FfiProposal?
func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue(_ param: FfiProposal?) async {
proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue = param
}
var proposeShieldingAccountMemoShieldingThresholdClosure: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)?
func setProposeShieldingAccountMemoShieldingThresholdClosure(_ param: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)?) async {
proposeShieldingAccountMemoShieldingThresholdClosure = param
var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)?
func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure(_ param: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)?) async {
proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure = param
}
func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi) async throws -> FfiProposal {
if let error = proposeShieldingAccountMemoShieldingThresholdThrowableError {
func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?) async throws -> FfiProposal? {
if let error = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError {
throw error
}
proposeShieldingAccountMemoShieldingThresholdCallsCount += 1
proposeShieldingAccountMemoShieldingThresholdReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold)
if let closure = proposeShieldingAccountMemoShieldingThresholdClosure {
return try await closure(account, memo, shieldingThreshold)
proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount += 1
proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold, transparentReceiver: transparentReceiver)
if let closure = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure {
return try await closure(account, memo, shieldingThreshold, transparentReceiver)
} else {
return proposeShieldingAccountMemoShieldingThresholdReturnValue
return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue
}
}
// MARK: - createProposedTransaction
// MARK: - createProposedTransactions
var createProposedTransactionProposalUskThrowableError: Error?
func setCreateProposedTransactionProposalUskThrowableError(_ param: Error?) async {
createProposedTransactionProposalUskThrowableError = param
var createProposedTransactionsProposalUskThrowableError: Error?
func setCreateProposedTransactionsProposalUskThrowableError(_ param: Error?) async {
createProposedTransactionsProposalUskThrowableError = param
}
var createProposedTransactionProposalUskCallsCount = 0
var createProposedTransactionProposalUskCalled: Bool {
return createProposedTransactionProposalUskCallsCount > 0
var createProposedTransactionsProposalUskCallsCount = 0
var createProposedTransactionsProposalUskCalled: Bool {
return createProposedTransactionsProposalUskCallsCount > 0
}
var createProposedTransactionProposalUskReceivedArguments: (proposal: FfiProposal, usk: UnifiedSpendingKey)?
var createProposedTransactionProposalUskReturnValue: Data!
func setCreateProposedTransactionProposalUskReturnValue(_ param: Data) async {
createProposedTransactionProposalUskReturnValue = param
var createProposedTransactionsProposalUskReceivedArguments: (proposal: FfiProposal, usk: UnifiedSpendingKey)?
var createProposedTransactionsProposalUskReturnValue: [Data]!
func setCreateProposedTransactionsProposalUskReturnValue(_ param: [Data]) async {
createProposedTransactionsProposalUskReturnValue = param
}
var createProposedTransactionProposalUskClosure: ((FfiProposal, UnifiedSpendingKey) async throws -> Data)?
func setCreateProposedTransactionProposalUskClosure(_ param: ((FfiProposal, UnifiedSpendingKey) async throws -> Data)?) async {
createProposedTransactionProposalUskClosure = param
var createProposedTransactionsProposalUskClosure: ((FfiProposal, UnifiedSpendingKey) async throws -> [Data])?
func setCreateProposedTransactionsProposalUskClosure(_ param: ((FfiProposal, UnifiedSpendingKey) async throws -> [Data])?) async {
createProposedTransactionsProposalUskClosure = param
}
func createProposedTransaction(proposal: FfiProposal, usk: UnifiedSpendingKey) async throws -> Data {
if let error = createProposedTransactionProposalUskThrowableError {
func createProposedTransactions(proposal: FfiProposal, usk: UnifiedSpendingKey) async throws -> [Data] {
if let error = createProposedTransactionsProposalUskThrowableError {
throw error
}
createProposedTransactionProposalUskCallsCount += 1
createProposedTransactionProposalUskReceivedArguments = (proposal: proposal, usk: usk)
if let closure = createProposedTransactionProposalUskClosure {
createProposedTransactionsProposalUskCallsCount += 1
createProposedTransactionsProposalUskReceivedArguments = (proposal: proposal, usk: usk)
if let closure = createProposedTransactionsProposalUskClosure {
return try await closure(proposal, usk)
} else {
return createProposedTransactionProposalUskReturnValue
return createProposedTransactionsProposalUskReturnValue
}
}

View File

@ -84,8 +84,8 @@ class RustBackendMockHelper {
await rustBackendMock.setGetNearestRewindHeightHeightReturnValue(-1)
await rustBackendMock.setPutUnspentTransparentOutputTxidIndexScriptValueHeightClosure() { _, _, _, _, _ in }
await rustBackendMock.setProposeTransferAccountToValueMemoThrowableError(ZcashError.rustCreateToAddress("mocked error"))
await rustBackendMock.setProposeShieldingAccountMemoShieldingThresholdThrowableError(ZcashError.rustShieldFunds("mocked error"))
await rustBackendMock.setCreateProposedTransactionProposalUskThrowableError(ZcashError.rustCreateToAddress("mocked error"))
await rustBackendMock.setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError(ZcashError.rustShieldFunds("mocked error"))
await rustBackendMock.setCreateProposedTransactionsProposalUskThrowableError(ZcashError.rustCreateToAddress("mocked error"))
await rustBackendMock.setDecryptAndStoreTransactionTxBytesMinedHeightThrowableError(ZcashError.rustDecryptAndStoreTransaction("mock fail"))
await rustBackendMock.setInitDataDbSeedClosure() { seed in