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/), 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
## 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 # 2.0.10 - 2024-02-12
## Added ## Added

View File

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

View File

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

View File

@ -16,7 +16,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.19.1"), .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/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: [ targets: [
.target( .target(

View File

@ -36,6 +36,64 @@ public protocol ClosureSynchronizer {
func getUnifiedAddress(accountIndex: Int, completion: @escaping (Result<UnifiedAddress, Error>) -> Void) func getUnifiedAddress(accountIndex: Int, completion: @escaping (Result<UnifiedAddress, Error>) -> Void)
func getTransparentAddress(accountIndex: Int, completion: @escaping (Result<TransparentAddress, 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( func sendToAddress(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi, zatoshi: Zatoshi,
@ -44,6 +102,7 @@ public protocol ClosureSynchronizer {
completion: @escaping (Result<ZcashTransaction.Overview, Error>) -> Void 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( func shieldFunds(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
memo: Memo, memo: Memo,

View File

@ -35,6 +35,61 @@ public protocol CombineSynchronizer {
func getUnifiedAddress(accountIndex: Int) -> SinglePublisher<UnifiedAddress, Error> func getUnifiedAddress(accountIndex: Int) -> SinglePublisher<UnifiedAddress, Error>
func getTransparentAddress(accountIndex: Int) -> SinglePublisher<TransparentAddress, 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( func sendToAddress(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi, zatoshi: Zatoshi,
@ -42,6 +97,7 @@ public protocol CombineSynchronizer {
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.")
func shieldFunds( func shieldFunds(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
memo: Memo, 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) #endif // swift(>=4.2)
/// A data structure that describes the inputs to be consumed and outputs to /// A data structure that describes a series of transactions to be created.
/// be produced in a proposed transaction.
struct FfiProposal { struct FfiProposal {
// SwiftProtobuf.Message conformance is added in an extension below. See the // SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages. // methods supported on all messages.
/// The version of this serialization format.
var protoVersion: UInt32 = 0 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 /// ZIP 321 serialized transaction request
var transactionRequest: String = String() 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. /// 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 /// Setting the anchor height to zero will disallow the use of any shielded
/// inputs. /// inputs.
@ -174,16 +202,7 @@ struct FfiProposal {
/// Clears the value of `balance`. Subsequent reads from it will return its default value. /// Clears the value of `balance`. Subsequent reads from it will return its default value.
mutating func clearBalance() {self._balance = nil} mutating func clearBalance() {self._balance = nil}
/// The fee rule used in constructing this proposal /// A flag indicating whether the step is for a shielding transaction,
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,
/// used for determining which OVK to select for wallet-internal outputs. /// used for determining which OVK to select for wallet-internal outputs.
var isShielding: Bool = false var isShielding: Bool = false
@ -194,8 +213,26 @@ struct FfiProposal {
fileprivate var _balance: FfiTransactionBalance? = nil fileprivate var _balance: FfiTransactionBalance? = nil
} }
/// The unique identifier and value for each proposed input. /// A mapping from ZIP 321 payment index to the output pool that has been chosen
struct FfiProposedInput { /// 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 // SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages. // methods supported on all messages.
@ -213,14 +250,113 @@ struct FfiProposedInput {
init() {} 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. /// The proposed change outputs and fee value.
struct FfiTransactionBalance { struct FfiTransactionBalance {
// SwiftProtobuf.Message conformance is added in an extension below. See the // SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages. // methods supported on all messages.
/// A list of change output values.
var proposedChange: [FfiChangeValue] = [] var proposedChange: [FfiChangeValue] = []
/// The fee to be paid by the proposed transaction, in zatoshis.
var feeRequired: UInt64 = 0 var feeRequired: UInt64 = 0
var unknownFields = SwiftProtobuf.UnknownStorage() var unknownFields = SwiftProtobuf.UnknownStorage()
@ -235,10 +371,14 @@ struct FfiChangeValue {
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages. // methods supported on all messages.
/// The value of a change output to be created, in zatoshis.
var value: UInt64 = 0 var value: UInt64 = 0
/// The value pool in which the change output should be created.
var valuePool: FfiValuePool = .poolNotSpecified 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 { var memo: FfiMemoBytes {
get {return _memo ?? FfiMemoBytes()} get {return _memo ?? FfiMemoBytes()}
set {_memo = newValue} set {_memo = newValue}
@ -273,7 +413,13 @@ struct FfiMemoBytes {
extension FfiValuePool: @unchecked Sendable {} extension FfiValuePool: @unchecked Sendable {}
extension FfiFeeRule: @unchecked Sendable {} extension FfiFeeRule: @unchecked Sendable {}
extension FfiProposal: @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: @unchecked Sendable {}
extension FfiProposedInput.OneOf_Value: @unchecked Sendable {}
extension FfiTransactionBalance: @unchecked Sendable {} extension FfiTransactionBalance: @unchecked Sendable {}
extension FfiChangeValue: @unchecked Sendable {} extension FfiChangeValue: @unchecked Sendable {}
extension FfiMemoBytes: @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 protoMessageName: String = _protobuf_package + ".Proposal"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "protoVersion"), 1: .same(proto: "protoVersion"),
2: .same(proto: "transactionRequest"), 2: .same(proto: "feeRule"),
3: .same(proto: "anchorHeight"), 3: .same(proto: "minTargetHeight"),
4: .same(proto: "inputs"), 4: .same(proto: "steps"),
5: .same(proto: "balance"),
6: .same(proto: "feeRule"),
7: .same(proto: "minTargetHeight"),
8: .same(proto: "isShielding"),
] ]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 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 // enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber { switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.protoVersion) }() 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 3: try { try decoder.decodeSingularUInt32Field(value: &self.anchorHeight) }()
case 4: try { try decoder.decodeRepeatedMessageField(value: &self.inputs) }() case 4: try { try decoder.decodeRepeatedMessageField(value: &self.inputs) }()
case 5: try { try decoder.decodeSingularMessageField(value: &self._balance) }() case 5: try { try decoder.decodeSingularMessageField(value: &self._balance) }()
case 6: try { try decoder.decodeSingularEnumField(value: &self.feeRule) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.isShielding) }()
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.minTargetHeight) }()
case 8: try { try decoder.decodeSingularBoolField(value: &self.isShielding) }()
default: break default: break
} }
} }
@ -338,11 +530,11 @@ extension FfiProposal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
// allocates stack space for every if/case branch local when no optimizations // allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182 // https://github.com/apple/swift-protobuf/issues/1182
if self.protoVersion != 0 {
try visitor.visitSingularUInt32Field(value: self.protoVersion, fieldNumber: 1)
}
if !self.transactionRequest.isEmpty { 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 { if self.anchorHeight != 0 {
try visitor.visitSingularUInt32Field(value: self.anchorHeight, fieldNumber: 3) 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 { if let v = self._balance {
try visitor.visitSingularMessageField(value: v, fieldNumber: 5) 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 { 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) try unknownFields.traverse(visitor: &visitor)
} }
static func ==(lhs: FfiProposal, rhs: FfiProposal) -> Bool { static func ==(lhs: FfiProposalStep, rhs: FfiProposalStep) -> Bool {
if lhs.protoVersion != rhs.protoVersion {return false}
if lhs.transactionRequest != rhs.transactionRequest {return false} if lhs.transactionRequest != rhs.transactionRequest {return false}
if lhs.paymentOutputPools != rhs.paymentOutputPools {return false}
if lhs.anchorHeight != rhs.anchorHeight {return false} if lhs.anchorHeight != rhs.anchorHeight {return false}
if lhs.inputs != rhs.inputs {return false} if lhs.inputs != rhs.inputs {return false}
if lhs._balance != rhs._balance {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.isShielding != rhs.isShielding {return false}
if lhs.unknownFields != rhs.unknownFields {return false} if lhs.unknownFields != rhs.unknownFields {return false}
return true return true
} }
} }
extension FfiProposedInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { extension FfiPaymentOutputPool: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".ProposedInput" 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 = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "txid"), 1: .same(proto: "txid"),
2: .same(proto: "valuePool"), 2: .same(proto: "valuePool"),
@ -419,7 +641,7 @@ extension FfiProposedInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
try unknownFields.traverse(visitor: &visitor) 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.txid != rhs.txid {return false}
if lhs.valuePool != rhs.valuePool {return false} if lhs.valuePool != rhs.valuePool {return false}
if lhs.index != rhs.index {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 { extension FfiTransactionBalance: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".TransactionBalance" static let protoMessageName: String = _protobuf_package + ".TransactionBalance"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View File

@ -6,12 +6,29 @@ syntax = "proto3";
package cash.z.wallet.sdk.ffi; package cash.z.wallet.sdk.ffi;
option swift_prefix = "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 // A data structure that describes the inputs to be consumed and outputs to
// be produced in a proposed transaction. // be produced in a proposed transaction.
message Proposal { message ProposalStep {
uint32 protoVersion = 1;
// ZIP 321 serialized transaction request // 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. // 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 // Setting the anchor height to zero will disallow the use of any shielded
// inputs. // inputs.
@ -21,16 +38,9 @@ message Proposal {
// The total value, fee value, and change outputs of the proposed // The total value, fee value, and change outputs of the proposed
// transaction // transaction
TransactionBalance balance = 5; TransactionBalance balance = 5;
// The fee rule used in constructing this proposal // A flag indicating whether the step is for a shielding transaction,
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,
// used for determining which OVK to select for wallet-internal outputs. // used for determining which OVK to select for wallet-internal outputs.
bool isShielding = 8; bool isShielding = 6;
} }
enum ValuePool { enum ValuePool {
@ -47,14 +57,45 @@ enum ValuePool {
Orchard = 3; Orchard = 3;
} }
// The unique identifier and value for each proposed input. // A mapping from ZIP 321 payment index to the output pool that has been chosen
message ProposedInput { // 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; bytes txid = 1;
ValuePool valuePool = 2; ValuePool valuePool = 2;
uint32 index = 3; uint32 index = 3;
uint64 value = 4; 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 // The fee rule used in constructing a Proposal
enum FeeRule { enum FeeRule {
// Protobuf requires that enums have a zero discriminant as the default // Protobuf requires that enums have a zero discriminant as the default
@ -72,15 +113,21 @@ enum FeeRule {
// The proposed change outputs and fee value. // The proposed change outputs and fee value.
message TransactionBalance { message TransactionBalance {
// A list of change output values.
repeated ChangeValue proposedChange = 1; repeated ChangeValue proposedChange = 1;
// The fee to be paid by the proposed transaction, in zatoshis.
uint64 feeRequired = 2; uint64 feeRequired = 2;
} }
// A proposed change output. If the transparent value pool is selected, // A proposed change output. If the transparent value pool is selected,
// the `memo` field must be null. // the `memo` field must be null.
message ChangeValue { message ChangeValue {
// The value of a change output to be created, in zatoshis.
uint64 value = 1; uint64 value = 1;
// The value pool in which the change output should be created.
ValuePool valuePool = 2; 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; MemoBytes memo = 3;
} }

View File

@ -13,7 +13,7 @@ let globalDBLock = NSLock()
actor ZcashRustBackend: ZcashRustBackendWelding { actor ZcashRustBackend: ZcashRustBackendWelding {
let minimumConfirmations: UInt32 = 10 let minimumConfirmations: UInt32 = 10
let useZIP317Fees = false let useZIP317Fees = true
let dbData: (String, UInt) let dbData: (String, UInt)
let fsBlockDbRoot: (String, UInt) let fsBlockDbRoot: (String, UInt)
@ -619,8 +619,9 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
func proposeShielding( func proposeShielding(
account: Int32, account: Int32,
memo: MemoBytes?, memo: MemoBytes?,
shieldingThreshold: Zatoshi shieldingThreshold: Zatoshi,
) async throws -> FfiProposal { transparentReceiver: String?
) async throws -> FfiProposal? {
globalDBLock.lock() globalDBLock.lock()
let proposal = zcashlc_propose_shielding( let proposal = zcashlc_propose_shielding(
dbData.0, dbData.0,
@ -628,6 +629,7 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
account, account,
memo?.bytes, memo?.bytes,
UInt64(shieldingThreshold.amount), UInt64(shieldingThreshold.amount),
transparentReceiver.map { [CChar]($0.utf8CString) },
networkType.networkId, networkType.networkId,
minimumConfirmations, minimumConfirmations,
useZIP317Fees useZIP317Fees
@ -646,44 +648,46 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
)) ))
} }
func createProposedTransaction( func createProposedTransactions(
proposal: FfiProposal, proposal: FfiProposal,
usk: UnifiedSpendingKey usk: UnifiedSpendingKey
) async throws -> Data { ) async throws -> [Data] {
var contiguousTxIdBytes = ContiguousArray<UInt8>([UInt8](repeating: 0x0, count: 32))
let proposalBytes = try proposal.serializedData(partial: false).bytes let proposalBytes = try proposal.serializedData(partial: false).bytes
globalDBLock.lock() globalDBLock.lock()
let success = contiguousTxIdBytes.withUnsafeMutableBufferPointer { txIdBytePtr in let txIdsPtr = proposalBytes.withUnsafeBufferPointer { proposalPtr in
proposalBytes.withUnsafeBufferPointer { proposalPtr in usk.bytes.withUnsafeBufferPointer { uskPtr in
usk.bytes.withUnsafeBufferPointer { uskPtr in zcashlc_create_proposed_transactions(
zcashlc_create_proposed_transaction( dbData.0,
dbData.0, dbData.1,
dbData.1, proposalPtr.baseAddress,
proposalPtr.baseAddress, UInt(proposalBytes.count),
UInt(proposalBytes.count), uskPtr.baseAddress,
uskPtr.baseAddress, UInt(usk.bytes.count),
UInt(usk.bytes.count), spendParamsPath.0,
spendParamsPath.0, spendParamsPath.1,
spendParamsPath.1, outputParamsPath.0,
outputParamsPath.0, outputParamsPath.1,
outputParamsPath.1, networkType.networkId
networkType.networkId, )
txIdBytePtr.baseAddress
)
}
} }
} }
globalDBLock.unlock() globalDBLock.unlock()
guard success else { guard let txIdsPtr else {
throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`createToAddress` failed with unknown error")) throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`createToAddress` failed with unknown error"))
} }
return contiguousTxIdBytes.withUnsafeBufferPointer { txIdBytePtr in defer { zcashlc_free_txids(txIdsPtr) }
Data(txIdBytePtr)
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 { 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 /// that can then be authorized and made ready for submission to the network with
/// `createProposedTransaction`. /// `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 account: index of the given account
/// - Parameter memo: the `Memo` for this transaction /// - 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. /// - Throws: `rustShieldFunds` if rust layer returns error.
func proposeShielding( func proposeShielding(
account: Int32, account: Int32,
memo: MemoBytes?, memo: MemoBytes?,
shieldingThreshold: Zatoshi shieldingThreshold: Zatoshi,
) async throws -> FfiProposal transparentReceiver: String?
) async throws -> FfiProposal?
/// Creates a transaction from the given proposal. /// Creates a transaction from the given proposal.
/// - Parameter proposal: the transaction proposal. /// - Parameter proposal: the transaction proposal.
/// - Parameter usk: `UnifiedSpendingKey` for the account that controls the funds to be spent. /// - Parameter usk: `UnifiedSpendingKey` for the account that controls the funds to be spent.
/// - Throws: `rustCreateToAddress`. /// - Throws: `rustCreateToAddress`.
func createProposedTransaction( func createProposedTransactions(
proposal: FfiProposal, proposal: FfiProposal,
usk: UnifiedSpendingKey usk: UnifiedSpendingKey
) async throws -> Data ) async throws -> [Data]
/// Gets the consensus branch id for the given height /// Gets the consensus branch id for the given height
/// - Parameter height: the height you what to know the branch id for /// - 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. /// - 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 /// - Returns the address or nil if account index is incorrect
func getTransparentAddress(accountIndex: Int) async throws -> TransparentAddress 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. /// Sends zatoshi.
/// - Parameter spendingKey: the `UnifiedSpendingKey` that allows spends to occur. /// - Parameter spendingKey: the `UnifiedSpendingKey` that allows spends to occur.
/// - Parameter zatoshi: the amount to send in Zatoshi. /// - Parameter zatoshi: the amount to send in Zatoshi.
/// - 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 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`. /// `SynchronizerErrors.notPrepared`.
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
func sendToAddress( func sendToAddress(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi, 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`. /// 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.
/// ///
/// 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`. /// `SynchronizerErrors.notPrepared`.
@available(*, deprecated, message: "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.")
func shieldFunds( func shieldFunds(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
memo: Memo, memo: Memo,
@ -227,7 +284,7 @@ public protocol Synchronizer: AnyObject {
/// Returns the latests UTXOs for the given address from the specified height on /// 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`. /// `SynchronizerErrors.notPrepared`.
func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) async throws -> RefreshedUTXOs 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 /// `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()` /// 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. /// `SynchronizerErrors.notPrepared` error.
/// ///
/// - Parameter policy: the rewind policy /// - Parameter policy: the rewind policy
@ -433,6 +490,19 @@ public enum RewindPolicy {
case quick 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 { extension InternalSyncStatus {
public static func == (lhs: InternalSyncStatus, rhs: InternalSyncStatus) -> Bool { public static func == (lhs: InternalSyncStatus, rhs: InternalSyncStatus) -> Bool {
switch (lhs, rhs) { 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( public func sendToAddress(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi, 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( public func shieldFunds(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
memo: Memo, 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( public func sendToAddress(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi, 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( public func shieldFunds(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
memo: Memo, memo: Memo,

View File

@ -265,6 +265,84 @@ public class SDKSynchronizer: Synchronizer {
// MARK: Synchronizer methods // 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( public func sendToAddress(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi, zatoshi: Zatoshi,
@ -312,13 +390,21 @@ public class SDKSynchronizer: Synchronizer {
throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds
} }
let transaction = try await transactionEncoder.createShieldingTransaction( guard let proposal = try await transactionEncoder.proposeShielding(
spendingKey: spendingKey, accountIndex: Int(spendingKey.account),
shieldingThreshold: shieldingThreshold, shieldingThreshold: shieldingThreshold,
memoBytes: memo.asMemoBytes(), 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() let encodedTx = try transaction.encodedTransaction()
try await transactionEncoder.submit(transaction: encodedTx) try await transactionEncoder.submit(transaction: encodedTx)
@ -339,14 +425,21 @@ public class SDKSynchronizer: Synchronizer {
throw ZcashError.synchronizerSendMemoToTransparentAddress throw ZcashError.synchronizerSendMemoToTransparentAddress
} }
let transaction = try await transactionEncoder.createTransaction( let proposal = try await transactionEncoder.proposeTransfer(
spendingKey: spendingKey, accountIndex: Int(spendingKey.account),
zatoshi: zatoshi, recipient: recipient.stringEncoded,
to: recipient.stringEncoded, amount: zatoshi,
memoBytes: memo?.asMemoBytes(), memoBytes: memo?.asMemoBytes()
from: Int(spendingKey.account)
) )
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() let encodedTransaction = try transaction.encodedTransaction()
try await transactionEncoder.submit(transaction: encodedTransaction) try await transactionEncoder.submit(transaction: encodedTransaction)

View File

@ -19,44 +19,58 @@ public enum TransactionEncoderError: Error {
} }
protocol TransactionEncoder { protocol TransactionEncoder {
/// Creates a transaction, throwing an exception whenever things are missing. When the provided wallet implementation /// Creates a proposal for transferring funds to the given recipient.
/// doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using
/// double-bangs for things).
/// ///
/// - Parameters: /// - Parameter accountIndex: the account from which to transfer funds.
/// - Parameter spendingKey: a `UnifiedSpendingKey` containing the spending key /// - Parameter recipient: string containing the recipient's address.
/// - Parameter zatoshi: the amount to send in `Zatoshi` /// - Parameter amount: the amount to send in Zatoshi.
/// - Parameter to: string containing the recipient address /// - 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.
/// - 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).
/// ///
/// - Parameters: /// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// - Parameter spendingKey: `UnifiedSpendingKey` to spend the UTXOs /// `SynchronizerErrors.notPrepared`.
/// - Parameter memoBytes: containing the memo (optional) func proposeTransfer(
/// - Parameter accountIndex: index of the account that will be used to send the funds accountIndex: Int,
/// - Throws: recipient: String,
/// - `walletTransEncoderShieldFundsMissingSaplingParams` if the sapling parameters aren't downloaded. amount: Zatoshi,
/// - Some `ZcashError.rust*` if the creation of transaction fails. memoBytes: MemoBytes?
func createShieldingTransaction( ) async throws -> Proposal
spendingKey: UnifiedSpendingKey,
/// 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, shieldingThreshold: Zatoshi,
memoBytes: MemoBytes?, memoBytes: MemoBytes?,
from accountIndex: Int transparentReceiver: String?
) async throws -> ZcashTransaction.Overview ) 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. /// submits a transaction to the Zcash peer-to-peer network.
/// - Parameter transaction: a transaction overview /// - Parameter transaction: a transaction overview

View File

@ -54,93 +54,61 @@ class WalletTransactionEncoder: TransactionEncoder {
logger: initializer.logger logger: initializer.logger
) )
} }
func createTransaction( func proposeTransfer(
spendingKey: UnifiedSpendingKey, accountIndex: Int,
zatoshi: Zatoshi, recipient: String,
to address: String, amount: Zatoshi,
memoBytes: MemoBytes?, memoBytes: MemoBytes?
from accountIndex: Int ) async throws -> Proposal {
) async throws -> ZcashTransaction.Overview { let proposal = try await rustBackend.proposeTransfer(
let txId = try await createSpend( account: Int32(accountIndex),
spendingKey: spendingKey, to: recipient,
zatoshi: zatoshi, value: amount.amount,
to: address, memo: memoBytes
memoBytes: memoBytes,
from: accountIndex
) )
logger.debug("transaction id: \(txId)") return Proposal(inner: proposal)
return try await repository.find(rawID: txId)
} }
func createSpend( func proposeShielding(
spendingKey: UnifiedSpendingKey, accountIndex: Int,
zatoshi: Zatoshi, shieldingThreshold: Zatoshi,
to address: String,
memoBytes: MemoBytes?, memoBytes: MemoBytes?,
from accountIndex: Int transparentReceiver: String? = nil
) async throws -> Data { ) 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 { guard ensureParams(spend: self.spendParamsURL, output: self.outputParamsURL) else {
throw ZcashError.walletTransEncoderCreateTransactionMissingSaplingParams throw ZcashError.walletTransEncoderCreateTransactionMissingSaplingParams
} }
// TODO: Expose the proposal in a way that enables querying its fee. let txIds = try await rustBackend.createProposedTransactions(
let proposal = try await rustBackend.proposeTransfer( proposal: proposal.inner,
account: Int32(spendingKey.account),
to: address,
value: zatoshi.amount,
memo: memoBytes
)
let txId = try await rustBackend.createProposedTransaction(
proposal: proposal,
usk: spendingKey usk: spendingKey
) )
return txId logger.debug("transaction ids: \(txIds)")
}
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)
}
func createShieldingSpend( var txs: [ZcashTransaction.Overview] = []
spendingKey: UnifiedSpendingKey,
shieldingThreshold: Zatoshi, for txId in txIds {
memo: MemoBytes?, txs.append(try await repository.find(rawID: txId))
accountIndex: Int
) async throws -> Data {
guard ensureParams(spend: self.spendParamsURL, output: self.outputParamsURL) else {
throw ZcashError.walletTransEncoderShieldFundsMissingSaplingParams
} }
// TODO: Expose the proposal in a way that enables querying its fee. return txs
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
} }
func submit( 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 // MARK: - sendToAddress
var sendToAddressSpendingKeyZatoshiToAddressMemoThrowableError: Error? var sendToAddressSpendingKeyZatoshiToAddressMemoThrowableError: Error?
@ -2756,67 +2828,67 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding {
// MARK: - proposeShielding // MARK: - proposeShielding
var proposeShieldingAccountMemoShieldingThresholdThrowableError: Error? var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError: Error?
func setProposeShieldingAccountMemoShieldingThresholdThrowableError(_ param: Error?) async { func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError(_ param: Error?) async {
proposeShieldingAccountMemoShieldingThresholdThrowableError = param proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError = param
} }
var proposeShieldingAccountMemoShieldingThresholdCallsCount = 0 var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount = 0
var proposeShieldingAccountMemoShieldingThresholdCalled: Bool { var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCalled: Bool {
return proposeShieldingAccountMemoShieldingThresholdCallsCount > 0 return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount > 0
} }
var proposeShieldingAccountMemoShieldingThresholdReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi)? var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?)?
var proposeShieldingAccountMemoShieldingThresholdReturnValue: FfiProposal! var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue: FfiProposal?
func setProposeShieldingAccountMemoShieldingThresholdReturnValue(_ param: FfiProposal) async { func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue(_ param: FfiProposal?) async {
proposeShieldingAccountMemoShieldingThresholdReturnValue = param proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue = param
} }
var proposeShieldingAccountMemoShieldingThresholdClosure: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)? var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)?
func setProposeShieldingAccountMemoShieldingThresholdClosure(_ param: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)?) async { func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure(_ param: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)?) async {
proposeShieldingAccountMemoShieldingThresholdClosure = param proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure = param
} }
func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi) async throws -> FfiProposal { func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?) async throws -> FfiProposal? {
if let error = proposeShieldingAccountMemoShieldingThresholdThrowableError { if let error = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError {
throw error throw error
} }
proposeShieldingAccountMemoShieldingThresholdCallsCount += 1 proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount += 1
proposeShieldingAccountMemoShieldingThresholdReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold) proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold, transparentReceiver: transparentReceiver)
if let closure = proposeShieldingAccountMemoShieldingThresholdClosure { if let closure = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure {
return try await closure(account, memo, shieldingThreshold) return try await closure(account, memo, shieldingThreshold, transparentReceiver)
} else { } else {
return proposeShieldingAccountMemoShieldingThresholdReturnValue return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue
} }
} }
// MARK: - createProposedTransaction // MARK: - createProposedTransactions
var createProposedTransactionProposalUskThrowableError: Error? var createProposedTransactionsProposalUskThrowableError: Error?
func setCreateProposedTransactionProposalUskThrowableError(_ param: Error?) async { func setCreateProposedTransactionsProposalUskThrowableError(_ param: Error?) async {
createProposedTransactionProposalUskThrowableError = param createProposedTransactionsProposalUskThrowableError = param
} }
var createProposedTransactionProposalUskCallsCount = 0 var createProposedTransactionsProposalUskCallsCount = 0
var createProposedTransactionProposalUskCalled: Bool { var createProposedTransactionsProposalUskCalled: Bool {
return createProposedTransactionProposalUskCallsCount > 0 return createProposedTransactionsProposalUskCallsCount > 0
} }
var createProposedTransactionProposalUskReceivedArguments: (proposal: FfiProposal, usk: UnifiedSpendingKey)? var createProposedTransactionsProposalUskReceivedArguments: (proposal: FfiProposal, usk: UnifiedSpendingKey)?
var createProposedTransactionProposalUskReturnValue: Data! var createProposedTransactionsProposalUskReturnValue: [Data]!
func setCreateProposedTransactionProposalUskReturnValue(_ param: Data) async { func setCreateProposedTransactionsProposalUskReturnValue(_ param: [Data]) async {
createProposedTransactionProposalUskReturnValue = param createProposedTransactionsProposalUskReturnValue = param
} }
var createProposedTransactionProposalUskClosure: ((FfiProposal, UnifiedSpendingKey) async throws -> Data)? var createProposedTransactionsProposalUskClosure: ((FfiProposal, UnifiedSpendingKey) async throws -> [Data])?
func setCreateProposedTransactionProposalUskClosure(_ param: ((FfiProposal, UnifiedSpendingKey) async throws -> Data)?) async { func setCreateProposedTransactionsProposalUskClosure(_ param: ((FfiProposal, UnifiedSpendingKey) async throws -> [Data])?) async {
createProposedTransactionProposalUskClosure = param createProposedTransactionsProposalUskClosure = param
} }
func createProposedTransaction(proposal: FfiProposal, usk: UnifiedSpendingKey) async throws -> Data { func createProposedTransactions(proposal: FfiProposal, usk: UnifiedSpendingKey) async throws -> [Data] {
if let error = createProposedTransactionProposalUskThrowableError { if let error = createProposedTransactionsProposalUskThrowableError {
throw error throw error
} }
createProposedTransactionProposalUskCallsCount += 1 createProposedTransactionsProposalUskCallsCount += 1
createProposedTransactionProposalUskReceivedArguments = (proposal: proposal, usk: usk) createProposedTransactionsProposalUskReceivedArguments = (proposal: proposal, usk: usk)
if let closure = createProposedTransactionProposalUskClosure { if let closure = createProposedTransactionsProposalUskClosure {
return try await closure(proposal, usk) return try await closure(proposal, usk)
} else { } else {
return createProposedTransactionProposalUskReturnValue return createProposedTransactionsProposalUskReturnValue
} }
} }

View File

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