From ee1f3cef7641d31c4c76805fd00cb677cb9bafd6 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 26 Aug 2026 10:33:34 -0400 Subject: [PATCH 1/2] chore(contract): sync ocp protos to ea6418c5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the Balance service: ocp.balance.v1.Balance, one unary RPC GetBalance. Nothing existing changed — the other nine generated Swift files are untouched, so no enum case was renumbered and the toolchain did not move. --- .../balance_v1_ocp_balance_service.grpc.swift | 201 ++++++++++++++++++ .../balance_v1_ocp_balance_service.pb.swift | 171 +++++++++++++++ ocp.lock | 4 +- proto/balance/v1/ocp_balance_service.proto | 30 +++ 4 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 Sources/OCPClientProtocol/balance_v1_ocp_balance_service.grpc.swift create mode 100644 Sources/OCPClientProtocol/balance_v1_ocp_balance_service.pb.swift create mode 100644 proto/balance/v1/ocp_balance_service.proto diff --git a/Sources/OCPClientProtocol/balance_v1_ocp_balance_service.grpc.swift b/Sources/OCPClientProtocol/balance_v1_ocp_balance_service.grpc.swift new file mode 100644 index 0000000..4af52f7 --- /dev/null +++ b/Sources/OCPClientProtocol/balance_v1_ocp_balance_service.grpc.swift @@ -0,0 +1,201 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: balance/v1/ocp_balance_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +// MARK: - ocp.balance.v1.Balance + +/// Namespace containing generated types for the "ocp.balance.v1.Balance" service. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public enum Ocp_Balance_V1_Balance { + /// Service descriptor for the "ocp.balance.v1.Balance" service. + public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "ocp.balance.v1.Balance") + /// Namespace for method metadata. + public enum Method { + /// Namespace for "GetBalance" metadata. + public enum GetBalance { + /// Request type for "GetBalance". + public typealias Input = Ocp_Balance_V1_GetBalanceRequest + /// Response type for "GetBalance". + public typealias Output = Ocp_Balance_V1_GetBalanceResponse + /// Descriptor for "GetBalance". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "ocp.balance.v1.Balance"), + method: "GetBalance" + ) + } + /// Descriptors for all methods in the "ocp.balance.v1.Balance" service. + public static let descriptors: [GRPCCore.MethodDescriptor] = [ + GetBalance.descriptor + ] + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "ocp.balance.v1.Balance" service. + public static let ocp_balance_v1_Balance = GRPCCore.ServiceDescriptor(fullyQualifiedService: "ocp.balance.v1.Balance") +} + +// MARK: ocp.balance.v1.Balance (client) + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Ocp_Balance_V1_Balance { + /// Generated client protocol for the "ocp.balance.v1.Balance" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + public protocol ClientProtocol: Sendable { + /// Call the "GetBalance" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetBalance returns balance data for any owner account + /// + /// - Parameters: + /// - request: A request containing a single `Ocp_Balance_V1_GetBalanceRequest` message. + /// - serializer: A serializer for `Ocp_Balance_V1_GetBalanceRequest` messages. + /// - deserializer: A deserializer for `Ocp_Balance_V1_GetBalanceResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getBalance( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "ocp.balance.v1.Balance" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + public struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + public init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "GetBalance" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetBalance returns balance data for any owner account + /// + /// - Parameters: + /// - request: A request containing a single `Ocp_Balance_V1_GetBalanceRequest` message. + /// - serializer: A serializer for `Ocp_Balance_V1_GetBalanceRequest` messages. + /// - deserializer: A deserializer for `Ocp_Balance_V1_GetBalanceResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func getBalance( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Ocp_Balance_V1_Balance.Method.GetBalance.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Ocp_Balance_V1_Balance.ClientProtocol { + /// Call the "GetBalance" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetBalance returns balance data for any owner account + /// + /// - Parameters: + /// - request: A request containing a single `Ocp_Balance_V1_GetBalanceRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func getBalance( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.getBalance( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } +} + +// Helpers providing sugared APIs for 'ClientProtocol' methods. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Ocp_Balance_V1_Balance.ClientProtocol { + /// Call the "GetBalance" method. + /// + /// > Source IDL Documentation: + /// > + /// > GetBalance returns balance data for any owner account + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func getBalance( + _ message: Ocp_Balance_V1_GetBalanceRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.getBalance( + request: request, + options: options, + onResponse: handleResponse + ) + } +} \ No newline at end of file diff --git a/Sources/OCPClientProtocol/balance_v1_ocp_balance_service.pb.swift b/Sources/OCPClientProtocol/balance_v1_ocp_balance_service.pb.swift new file mode 100644 index 0000000..0f46005 --- /dev/null +++ b/Sources/OCPClientProtocol/balance_v1_ocp_balance_service.pb.swift @@ -0,0 +1,171 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: balance/v1/ocp_balance_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +public struct Ocp_Balance_V1_GetBalanceRequest: 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. + + public var owner: Ocp_Common_V1_SolanaAccountId { + get {return _owner ?? Ocp_Common_V1_SolanaAccountId()} + set {_owner = newValue} + } + /// Returns true if `owner` has been explicitly set. + public var hasOwner: Bool {return self._owner != nil} + /// Clears the value of `owner`. Subsequent reads from it will return its default value. + public mutating func clearOwner() {self._owner = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _owner: Ocp_Common_V1_SolanaAccountId? = nil +} + +public struct Ocp_Balance_V1_GetBalanceResponse: 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. + + public var result: Ocp_Balance_V1_GetBalanceResponse.Result = .ok + + public var coreMintValue: UInt64 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum Result: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case ok // = 0 + case denied // = 1 + case notFound // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .ok + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .denied + case 2: self = .notFound + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .ok: return 0 + case .denied: return 1 + case .notFound: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Ocp_Balance_V1_GetBalanceResponse.Result] = [ + .ok, + .denied, + .notFound, + ] + + } + + public init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "ocp.balance.v1" + +extension Ocp_Balance_V1_GetBalanceRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".GetBalanceRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}owner\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._owner) }() + 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._owner { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Ocp_Balance_V1_GetBalanceRequest, rhs: Ocp_Balance_V1_GetBalanceRequest) -> Bool { + if lhs._owner != rhs._owner {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Ocp_Balance_V1_GetBalanceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".GetBalanceResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0\u{3}core_mint_value\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.decodeSingularEnumField(value: &self.result) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.coreMintValue) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.result != .ok { + try visitor.visitSingularEnumField(value: self.result, fieldNumber: 1) + } + if self.coreMintValue != 0 { + try visitor.visitSingularUInt64Field(value: self.coreMintValue, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Ocp_Balance_V1_GetBalanceResponse, rhs: Ocp_Balance_V1_GetBalanceResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs.coreMintValue != rhs.coreMintValue {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Ocp_Balance_V1_GetBalanceResponse.Result: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}NOT_FOUND\0") +} diff --git a/ocp.lock b/ocp.lock index fc8ee14..a4cda69 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: 04281923635228c137cb9255b24593a468b01b4e -subject: Add MarketCapMetrics to Mint (#62) +commit: ea6418c5561e16771d456062be2fcbd3ddeb9caf +subject: Add the Balance RPC service (#63) diff --git a/proto/balance/v1/ocp_balance_service.proto b/proto/balance/v1/ocp_balance_service.proto new file mode 100644 index 0000000..fe217fb --- /dev/null +++ b/proto/balance/v1/ocp_balance_service.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package ocp.balance.v1; + +option go_package = "github.com/code-payments/ocp-protobuf-api/generated/go/balance/v1;balance"; +option java_package = "com.codeinc.opencode.gen.balance.v1"; +option objc_class_prefix = "CPBBalanceV1"; + +import "common/v1/model.proto"; +import "validate/validate.proto"; + +service Balance { + // GetBalance returns balance data for any owner account + rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse); +} + +message GetBalanceRequest { + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; +} + +message GetBalanceResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + NOT_FOUND = 2; + } + + uint64 core_mint_value = 2; +} From 7ce0f7c1d86b80ae30d69f8da2a05f0731701946 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 26 Aug 2026 11:15:24 -0400 Subject: [PATCH 2/2] chore(release): build release notes from a changelog publish.yml created the GitHub release with --generate-notes, which lists merged PR titles. For a contract package that says nothing useful: 0.1.0 released with a body that was only a "Full Changelog" link, so a consumer reading it could not tell what was in the API. Release notes now come from a CHANGELOG.md section matching the published version, written in the PR that syncs the contract while the diff is still at hand. A version with no section fails the publish, and it fails before the upload, since Maven Central will not take a version number twice. --- .github/workflows/publish.yml | 19 ++++++++++++++++++- .gitignore | 3 +++ CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 76ab466..25223b8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,6 +50,23 @@ jobs: exit 1 fi + # The release notes come from CHANGELOG.md, not from generated commit titles, so a + # missing entry is a missing release note. Fail here rather than after the upload. + - name: Extract the release notes + run: | + set -euo pipefail + awk -v ver="${{ inputs.version }}" ' + $0 == "## " ver { found = 1; next } + found && /^## / { exit } + found { print } + ' CHANGELOG.md > release-notes.md + if [ ! -s release-notes.md ]; then + echo "::error::CHANGELOG.md has no '## ${{ inputs.version }}' section. Add one describing the contract change before publishing." + exit 1 + fi + echo "Release notes for ${{ inputs.version }}:" + cat release-notes.md + - name: Validate Gradle wrapper uses: gradle/actions/wrapper-validation@v4 @@ -122,7 +139,7 @@ jobs: run: | gh release create "${{ inputs.version }}" \ --title "${{ inputs.version }}" \ - --generate-notes + --notes-file release-notes.md - name: Summary run: | diff --git a/.gitignore b/.gitignore index b4512fa..496a131 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ local.properties # Pinned codegen toolchain, installed by scripts/install-swift-toolchain.sh. .tools/ + +# Written by publish.yml when it extracts release notes from CHANGELOG.md +release-notes.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4cd30de --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +Contract changes from a consumer's point of view: what appeared, what changed shape, and what breaks +if you upgrade. Field and enum renumbering matters more than its diff size suggests, so it gets +called out explicitly even when nothing else did. + +`publish.yml` reads the section matching the version it is publishing and uses it as the GitHub +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.2.0 + +Synced to [`ocp-protobuf-api@ea6418c5`](https://github.com/code-payments/ocp-protobuf-api/commit/ea6418c5561e16771d456062be2fcbd3ddeb9caf). + +### Added + +- `ocp.balance.v1.Balance`, a new service with one unary RPC, `GetBalance`. It takes an owner account + and returns `core_mint_value`, a `uint64` in quarks, alongside a result enum of `OK`, `DENIED`, and + `NOT_FOUND`. + + `GetBalanceRequest` carries no auth or signature field, unlike every other OCP request. It reads + balance for any owner account rather than the caller's own, so there is nothing to sign. + +Nothing existing changed. No field number or enum value moved, so upgrading from `0.1.0` needs no +consumer changes. + +## 0.1.0 + +First release. The OCP contract is now generated once here and published for both platforms, replacing +the copies each app vendored and generated for itself. + +- Kotlin, on Maven Central as `com.flipcash:ocp-client-protocol`, under `com.codeinc.opencode.gen.*`. +- Swift, as the `OCPClientProtocol` module. The git tag is the SPM release.