From e9177a28f73d19f81849108e9b4978a10a08a582 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 6 Mar 2024 03:06:57 +0000 Subject: [PATCH] Migrate to in-progress version of FFI backend 0.6.0 Includes: - Multi-step transaction proposals. - Changes to support `Synchronizer.proposeShielding` API changes. --- .../xcshareddata/swiftpm/Package.resolved | 3 +- Package.resolved | 3 +- Package.swift | 3 +- .../ZcashLightClientKit/Model/Proposal.swift | 4 +- .../Service/GRPC/ProtoBuf/proposal.pb.swift | 472 ++++++++++++++++-- .../GRPC/ProtoBuf/proto/proposal.proto | 75 ++- .../Rust/ZcashRustBackend.swift | 66 +-- .../Rust/ZcashRustBackendWelding.swift | 4 +- .../WalletTransactionEncoder.swift | 13 +- .../AutoMockable.generated.swift | 40 +- Tests/TestUtils/Stubs.swift | 2 +- 11 files changed, 567 insertions(+), 118 deletions(-) diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 03537a49..145b2a4b 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -176,8 +176,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "c90afd6cc092468e71810bc715ddb49be8210b75", - "version" : "0.5.1" + "revision" : "789d0c068fb32e2ab149cdd785f16e0ac88f3594" } } ], diff --git a/Package.resolved b/Package.resolved index 8b8c4e8a..7cefa11b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -122,8 +122,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "c90afd6cc092468e71810bc715ddb49be8210b75", - "version" : "0.5.1" + "revision" : "789d0c068fb32e2ab149cdd785f16e0ac88f3594" } } ], diff --git a/Package.swift b/Package.swift index 4e410e85..dd4795c2 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,8 @@ 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") + // Compiled from revision `97e09ed3709ae9f26226587bec852a725bc783a4`. + .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "789d0c068fb32e2ab149cdd785f16e0ac88f3594") ], targets: [ .target( diff --git a/Sources/ZcashLightClientKit/Model/Proposal.swift b/Sources/ZcashLightClientKit/Model/Proposal.swift index eb6ba8b2..6a238f66 100644 --- a/Sources/ZcashLightClientKit/Model/Proposal.swift +++ b/Sources/ZcashLightClientKit/Model/Proposal.swift @@ -13,7 +13,9 @@ public struct Proposal: Equatable { /// Returns the total fee to be paid across all proposed transactions, in zatoshis. public func totalFeeRequired() -> Zatoshi { - Zatoshi(Int64(inner.balance.feeRequired)) + inner.steps.reduce(Zatoshi.zero) { acc, step in + acc + Zatoshi(Int64(step.balance.feeRequired)) + } } } diff --git a/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift index 3a7e3701..3a6bf464 100644 --- a/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift +++ b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift @@ -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(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(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(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(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(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(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(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(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(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(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(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 = [ diff --git a/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto index 3ebee76f..1af5ec27 100644 --- a/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto +++ b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto @@ -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; } diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 290bce63..fa3b4a05 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -622,10 +622,6 @@ actor ZcashRustBackend: ZcashRustBackendWelding { shieldingThreshold: Zatoshi, transparentReceiver: String? ) async throws -> FfiProposal? { - if transparentReceiver != nil { - throw ZcashError.rustScanBlocks("TODO: Implement transparentReceiver support in FFI") - } - globalDBLock.lock() let proposal = zcashlc_propose_shielding( dbData.0, @@ -633,6 +629,7 @@ actor ZcashRustBackend: ZcashRustBackendWelding { account, memo?.bytes, UInt64(shieldingThreshold.amount), + transparentReceiver.map { [CChar]($0.utf8CString) }, networkType.networkId, minimumConfirmations, useZIP317Fees @@ -651,44 +648,46 @@ actor ZcashRustBackend: ZcashRustBackendWelding { )) } - func createProposedTransaction( + func createProposedTransactions( proposal: FfiProposal, usk: UnifiedSpendingKey - ) async throws -> Data { - var contiguousTxIdBytes = ContiguousArray([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 { @@ -828,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) + } + } +} diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index fd6cc03f..8843dea1 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -233,10 +233,10 @@ protocol ZcashRustBackendWelding { /// - 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 diff --git a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift index 5e9ed9bd..31e4f796 100644 --- a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift @@ -95,13 +95,20 @@ class WalletTransactionEncoder: TransactionEncoder { throw ZcashError.walletTransEncoderCreateTransactionMissingSaplingParams } - let txId = try await rustBackend.createProposedTransaction( + let txIds = try await rustBackend.createProposedTransactions( proposal: proposal.inner, usk: spendingKey ) - logger.debug("transaction id: \(txId)") - return [try await repository.find(rawID: txId)] + logger.debug("transaction ids: \(txIds)") + + var txs: [ZcashTransaction.Overview] = [] + + for txId in txIds { + txs.append(try await repository.find(rawID: txId)) + } + + return txs } func submit( diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index ebb863bc..156d078a 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -2859,36 +2859,36 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } } - // 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 } } diff --git a/Tests/TestUtils/Stubs.swift b/Tests/TestUtils/Stubs.swift index dbc62987..f5f33521 100644 --- a/Tests/TestUtils/Stubs.swift +++ b/Tests/TestUtils/Stubs.swift @@ -85,7 +85,7 @@ class RustBackendMockHelper { await rustBackendMock.setPutUnspentTransparentOutputTxidIndexScriptValueHeightClosure() { _, _, _, _, _ in } await rustBackendMock.setProposeTransferAccountToValueMemoThrowableError(ZcashError.rustCreateToAddress("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.setInitDataDbSeedClosure() { seed in