diff --git a/CHANGELOG.md b/CHANGELOG.md index 927e263..ec2e987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,32 @@ called out explicitly even when nothing else did. release notes, so a version with no entry here does not release. Write the entry in the same PR that syncs the contract, while the diff is still in front of you. +## 0.8.0 + +Synced to [`ocp-protobuf-api@6f70c0d8`](https://github.com/code-payments/ocp-protobuf-api/commit/6f70c0d8e832531fd006bbb02fcb2c5d07e0c247), +picking up [#70](https://github.com/code-payments/ocp-protobuf-api/pull/70). Only `transaction.v1` moved. + +A new intent type pays several OCP primary accounts from one intent. It is additive, so the upgrade +is free. A client that switches over `Metadata.type` exhaustively gets one more case to handle. + +### Added + +- `SendPublicMultiPaymentMetadata`, carried as `Metadata.send_public_multi_payment` (oneof field 5). + It holds a `source`, a `mint`, and 2 to 10 `payments`, each one a `destination`, an optional + `destination_owner`, and either `server_exchange_data` or `client_exchange_data`. The intent emits + one `NoPrivacyTransferAction` from the source primary account per payment. +- Every destination has to be an OCP-managed primary account for the intent's mint. Destinations + and their owners must be unique and must differ from the source. Every payment has to share the + same currency, the same verified core-mint rate and, for launchpad currencies, the same verified + reserve state. When a destination's owner is an existing OCP user with no primary account on the + mint yet, the server opens it, and `destination_owner` is required for that payment. The server + may cap the payment count below 10. + +### Unchanged + +Nothing was renumbered, and nothing existing changed type or meaning. No service, RPC, message or +enum case was removed. + ## 0.7.0 Synced to [`ocp-protobuf-api@00ea80d0`](https://github.com/code-payments/ocp-protobuf-api/commit/00ea80d00cf1ce76216937b60cfd60adfd37dc99), diff --git a/Sources/OCPClientProtocol/ContractInfo.swift b/Sources/OCPClientProtocol/ContractInfo.swift index c12f15f..f062429 100644 --- a/Sources/OCPClientProtocol/ContractInfo.swift +++ b/Sources/OCPClientProtocol/ContractInfo.swift @@ -1,8 +1,8 @@ // Generated by scripts/generate-swift.sh. Do not edit. public enum OCPContractInfo { - public static let version = "0.7.0-dev" - public static let protoCommit = "00ea80d00cf1ce76216937b60cfd60adfd37dc99" + public static let version = "0.8.0-dev" + public static let protoCommit = "6f70c0d8e832531fd006bbb02fcb2c5d07e0c247" public static var isLocal: Bool { protoCommit == localSentinel } diff --git a/Sources/OCPClientProtocol/transaction_v1_ocp_transaction_service.pb.swift b/Sources/OCPClientProtocol/transaction_v1_ocp_transaction_service.pb.swift index 9938231..b7a1edb 100644 --- a/Sources/OCPClientProtocol/transaction_v1_ocp_transaction_service.pb.swift +++ b/Sources/OCPClientProtocol/transaction_v1_ocp_transaction_service.pb.swift @@ -2297,6 +2297,14 @@ public struct Ocp_Transaction_V1_Metadata: Sendable { set {type = .publicDistribution(newValue)} } + public var sendPublicMultiPayment: Ocp_Transaction_V1_SendPublicMultiPaymentMetadata { + get { + if case .sendPublicMultiPayment(let v)? = type {return v} + return Ocp_Transaction_V1_SendPublicMultiPaymentMetadata() + } + set {type = .sendPublicMultiPayment(newValue)} + } + /// Optional app-level metadata public var appMetadata: Ocp_Transaction_V1_AppMetadata { get {return _appMetadata ?? Ocp_Transaction_V1_AppMetadata()} @@ -2314,6 +2322,7 @@ public struct Ocp_Transaction_V1_Metadata: Sendable { case sendPublicPayment(Ocp_Transaction_V1_SendPublicPaymentMetadata) case receivePaymentsPublicly(Ocp_Transaction_V1_ReceivePaymentsPubliclyMetadata) case publicDistribution(Ocp_Transaction_V1_PublicDistributionMetadata) + case sendPublicMultiPayment(Ocp_Transaction_V1_SendPublicMultiPaymentMetadata) } @@ -2647,6 +2656,131 @@ public struct Ocp_Transaction_V1_PublicDistributionMetadata: Sendable { fileprivate var _mint: Ocp_Common_V1_SolanaAccountId? = nil } +/// Send payments publicly from the user's primary account to multiple OCP +/// primary accounts. +/// +/// Action Spec: +/// +/// for payment in payments +/// actions.push_back(NoPrivacyTransferAction(PRIMARY, payment.destination, payment.ExchangeData.Quarks)) +/// +/// Notes: +/// - Every destination must be a PRIMARY account of the intent mint that is +/// managed by OCP. Withdrawals, indirect sends and fee payments are not +/// supported. +/// - If a destination owner is an existing OCP user without a PRIMARY account +/// on the intent mint, the server auto-opens it as part of the intent. The +/// destination must be the owner's derived PRIMARY timelock vault for the +/// intent mint, and destination_owner must be provided. +/// - Destinations, and the owners of destinations, must be unique and cannot +/// be the source or its owner. +/// - Every payment's client exchange data must use the same currency, verified +/// core mint fiat exchange rate and, for launchpad currencies, verified +/// reserve state. +/// - The server may enforce a lower maximum number of payments than allowed +/// by validation here. +public struct Ocp_Transaction_V1_SendPublicMultiPaymentMetadata: Sendable { + // 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 source account where funds will be sent from. Currently, this is always + /// the user's primary account. + public var source: Ocp_Common_V1_SolanaAccountId { + get {return _source ?? Ocp_Common_V1_SolanaAccountId()} + set {_source = newValue} + } + /// Returns true if `source` has been explicitly set. + public var hasSource: Bool {return self._source != nil} + /// Clears the value of `source`. Subsequent reads from it will return its default value. + public mutating func clearSource() {self._source = nil} + + /// The set of payments + public var payments: [Ocp_Transaction_V1_SendPublicMultiPaymentMetadata.Payment] = [] + + /// The mint that this intent will be operating against + public var mint: Ocp_Common_V1_SolanaAccountId { + get {return _mint ?? Ocp_Common_V1_SolanaAccountId()} + set {_mint = newValue} + } + /// Returns true if `mint` has been explicitly set. + public var hasMint: Bool {return self._mint != nil} + /// Clears the value of `mint`. Subsequent reads from it will return its default value. + public mutating func clearMint() {self._mint = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public struct Payment: Sendable { + // 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 destination primary account to send funds to + public var destination: Ocp_Common_V1_SolanaAccountId { + get {return _destination ?? Ocp_Common_V1_SolanaAccountId()} + set {_destination = newValue} + } + /// Returns true if `destination` has been explicitly set. + public var hasDestination: Bool {return self._destination != nil} + /// Clears the value of `destination`. Subsequent reads from it will return its default value. + public mutating func clearDestination() {self._destination = nil} + + /// The owner of the destination account, which is required when the + /// destination PRIMARY account doesn't exist yet and will be auto-opened. + /// It can be omitted when the destination already exists. + public var destinationOwner: Ocp_Common_V1_SolanaAccountId { + get {return _destinationOwner ?? Ocp_Common_V1_SolanaAccountId()} + set {_destinationOwner = newValue} + } + /// Returns true if `destinationOwner` has been explicitly set. + public var hasDestinationOwner: Bool {return self._destinationOwner != nil} + /// Clears the value of `destinationOwner`. Subsequent reads from it will return its default value. + public mutating func clearDestinationOwner() {self._destinationOwner = nil} + + /// The exchange data of funds being sent to the destination + public var exchangeData: Ocp_Transaction_V1_SendPublicMultiPaymentMetadata.Payment.OneOf_ExchangeData? = nil + + /// Provided by server for submitted intents + public var serverExchangeData: Ocp_Transaction_V1_ExchangeData { + get { + if case .serverExchangeData(let v)? = exchangeData {return v} + return Ocp_Transaction_V1_ExchangeData() + } + set {exchangeData = .serverExchangeData(newValue)} + } + + /// Provided by clients when submitting new intents + public var clientExchangeData: Ocp_Transaction_V1_VerifiedExchangeData { + get { + if case .clientExchangeData(let v)? = exchangeData {return v} + return Ocp_Transaction_V1_VerifiedExchangeData() + } + set {exchangeData = .clientExchangeData(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The exchange data of funds being sent to the destination + public enum OneOf_ExchangeData: Equatable, Sendable { + /// Provided by server for submitted intents + case serverExchangeData(Ocp_Transaction_V1_ExchangeData) + /// Provided by clients when submitting new intents + case clientExchangeData(Ocp_Transaction_V1_VerifiedExchangeData) + + } + + public init() {} + + fileprivate var _destination: Ocp_Common_V1_SolanaAccountId? = nil + fileprivate var _destinationOwner: Ocp_Common_V1_SolanaAccountId? = nil + } + + public init() {} + + fileprivate var _source: Ocp_Common_V1_SolanaAccountId? = nil + fileprivate var _mint: Ocp_Common_V1_SolanaAccountId? = nil +} + /// Action is a well-defined, ordered and small set of transactions or virtual instructions /// for a unit of work that the client wants to perform on the blockchain. Clients provide /// parameters known to them in the action. @@ -5867,7 +6001,7 @@ extension Ocp_Transaction_V1_GetPendingSwapsResponse.Result: SwiftProtobuf._Prot extension Ocp_Transaction_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Metadata" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}open_accounts\0\u{3}send_public_payment\0\u{3}receive_payments_publicly\0\u{3}public_distribution\0\u{4}\u{6}app_metadata\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}open_accounts\0\u{3}send_public_payment\0\u{3}receive_payments_publicly\0\u{3}public_distribution\0\u{3}send_public_multi_payment\0\u{4}\u{5}app_metadata\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -5927,6 +6061,19 @@ extension Ocp_Transaction_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Mes self.type = .publicDistribution(v) } }() + case 5: try { + var v: Ocp_Transaction_V1_SendPublicMultiPaymentMetadata? + var hadOneofValue = false + if let current = self.type { + hadOneofValue = true + if case .sendPublicMultiPayment(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.type = .sendPublicMultiPayment(v) + } + }() case 10: try { try decoder.decodeSingularMessageField(value: &self._appMetadata) }() default: break } @@ -5955,6 +6102,10 @@ extension Ocp_Transaction_V1_Metadata: SwiftProtobuf.Message, SwiftProtobuf._Mes guard case .publicDistribution(let v)? = self.type else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 4) }() + case .sendPublicMultiPayment?: try { + guard case .sendPublicMultiPayment(let v)? = self.type else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() case nil: break } try { if let v = self._appMetadata { @@ -6291,6 +6442,127 @@ extension Ocp_Transaction_V1_PublicDistributionMetadata.Distribution: SwiftProto } } +extension Ocp_Transaction_V1_SendPublicMultiPaymentMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SendPublicMultiPaymentMetadata" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}source\0\u{1}payments\0\u{1}mint\0") + + public 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.decodeSingularMessageField(value: &self._source) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.payments) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._mint) }() + default: break + } + } + } + + public 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 + try { if let v = self._source { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.payments.isEmpty { + try visitor.visitRepeatedMessageField(value: self.payments, fieldNumber: 2) + } + try { if let v = self._mint { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Ocp_Transaction_V1_SendPublicMultiPaymentMetadata, rhs: Ocp_Transaction_V1_SendPublicMultiPaymentMetadata) -> Bool { + if lhs._source != rhs._source {return false} + if lhs.payments != rhs.payments {return false} + if lhs._mint != rhs._mint {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Ocp_Transaction_V1_SendPublicMultiPaymentMetadata.Payment: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Ocp_Transaction_V1_SendPublicMultiPaymentMetadata.protoMessageName + ".Payment" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}destination\0\u{3}destination_owner\0\u{3}server_exchange_data\0\u{3}client_exchange_data\0") + + public 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.decodeSingularMessageField(value: &self._destination) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._destinationOwner) }() + case 3: try { + var v: Ocp_Transaction_V1_ExchangeData? + var hadOneofValue = false + if let current = self.exchangeData { + hadOneofValue = true + if case .serverExchangeData(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.exchangeData = .serverExchangeData(v) + } + }() + case 4: try { + var v: Ocp_Transaction_V1_VerifiedExchangeData? + var hadOneofValue = false + if let current = self.exchangeData { + hadOneofValue = true + if case .clientExchangeData(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.exchangeData = .clientExchangeData(v) + } + }() + default: break + } + } + } + + public 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 + try { if let v = self._destination { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._destinationOwner { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + switch self.exchangeData { + case .serverExchangeData?: try { + guard case .serverExchangeData(let v)? = self.exchangeData else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .clientExchangeData?: try { + guard case .clientExchangeData(let v)? = self.exchangeData else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Ocp_Transaction_V1_SendPublicMultiPaymentMetadata.Payment, rhs: Ocp_Transaction_V1_SendPublicMultiPaymentMetadata.Payment) -> Bool { + if lhs._destination != rhs._destination {return false} + if lhs._destinationOwner != rhs._destinationOwner {return false} + if lhs.exchangeData != rhs.exchangeData {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Ocp_Transaction_V1_Action: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Action" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{3}open_account\0\u{3}no_privacy_transfer\0\u{3}no_privacy_withdraw\0\u{3}fee_payment\0") diff --git a/ocp.lock b/ocp.lock index 8e17ded..25e7458 100644 --- a/ocp.lock +++ b/ocp.lock @@ -1,4 +1,4 @@ # Pinned upstream contract. Regenerate with scripts/sync-protos.sh . upstream: code-payments/ocp-protobuf-api -commit: 00ea80d00cf1ce76216937b60cfd60adfd37dc99 -subject: GetBalances now returns per-mint quarks and can exclude per-mint balances (#69) +commit: 6f70c0d8e832531fd006bbb02fcb2c5d07e0c247 +subject: Add SendPublicMultiPaymentMetadata intent type (#70) diff --git a/proto/transaction/v1/ocp_transaction_service.proto b/proto/transaction/v1/ocp_transaction_service.proto index 8f1ed95..b222348 100644 --- a/proto/transaction/v1/ocp_transaction_service.proto +++ b/proto/transaction/v1/ocp_transaction_service.proto @@ -918,6 +918,7 @@ message Metadata { SendPublicPaymentMetadata send_public_payment = 2; ReceivePaymentsPubliclyMetadata receive_payments_publicly = 3; PublicDistributionMetadata public_distribution = 4; + SendPublicMultiPaymentMetadata send_public_multi_payment = 5; } // Optional app-level metadata @@ -1062,6 +1063,64 @@ message PublicDistributionMetadata { common.v1.SolanaAccountId mint = 3 [(validate.rules).message.required = true];; } +// Send payments publicly from the user's primary account to multiple OCP +// primary accounts. +// +// Action Spec: +// +// for payment in payments +// actions.push_back(NoPrivacyTransferAction(PRIMARY, payment.destination, payment.ExchangeData.Quarks)) +// +// Notes: +// - Every destination must be a PRIMARY account of the intent mint that is +// managed by OCP. Withdrawals, indirect sends and fee payments are not +// supported. +// - If a destination owner is an existing OCP user without a PRIMARY account +// on the intent mint, the server auto-opens it as part of the intent. The +// destination must be the owner's derived PRIMARY timelock vault for the +// intent mint, and destination_owner must be provided. +// - Destinations, and the owners of destinations, must be unique and cannot +// be the source or its owner. +// - Every payment's client exchange data must use the same currency, verified +// core mint fiat exchange rate and, for launchpad currencies, verified +// reserve state. +// - The server may enforce a lower maximum number of payments than allowed +// by validation here. +message SendPublicMultiPaymentMetadata { + // The source account where funds will be sent from. Currently, this is always + // the user's primary account. + common.v1.SolanaAccountId source = 1 [(validate.rules).message.required = true]; + + // The set of payments + repeated Payment payments = 2 [(validate.rules).repeated = { + min_items: 2, + max_items: 10, // Arbitrary + }]; + message Payment { + // The destination primary account to send funds to + common.v1.SolanaAccountId destination = 1 [(validate.rules).message.required = true]; + + // The owner of the destination account, which is required when the + // destination PRIMARY account doesn't exist yet and will be auto-opened. + // It can be omitted when the destination already exists. + common.v1.SolanaAccountId destination_owner = 2; + + // The exchange data of funds being sent to the destination + oneof exchange_data { + option (validate.required) = true; + + // Provided by server for submitted intents + ExchangeData server_exchange_data = 3; + + // Provided by clients when submitting new intents + VerifiedExchangeData client_exchange_data = 4; + } + } + + // The mint that this intent will be operating against + common.v1.SolanaAccountId mint = 3 [(validate.rules).message.required = true]; +} + // // Action Definitions //