From e02c61616e0111df9ea9d3381ab4f21331f0ca7d Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 26 Aug 2026 10:49:13 -0400 Subject: [PATCH 1/2] feat(payments): scaffold Balance service for GetBalance Adds the app-side service/client layering for the new ocp.balance.v1.Balance RPC: BalanceService wraps the generated client, ErrorGetBalance maps the proto result enum and conforms to TransportClassifiableError, and Client+Balance exposes an async getBalance(owner:) wrapper. GetBalanceRequest carries no auth field, so this takes a bare PublicKey rather than a signing KeyPair, unlike every other Payments API request. --- .../Clients/Payments API/Client+Balance.swift | 17 +++++ .../Clients/Payments API/Client.swift | 2 + .../Services/BalanceService.swift | 72 +++++++++++++++++++ .../TransportClassificationTests.swift | 1 + 4 files changed, 92 insertions(+) create mode 100644 FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client+Balance.swift create mode 100644 FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift diff --git a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client+Balance.swift b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client+Balance.swift new file mode 100644 index 000000000..2e4e0e1d1 --- /dev/null +++ b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client+Balance.swift @@ -0,0 +1,17 @@ +// +// Client+Balance.swift +// FlipcashCore +// + +import Foundation +import FlipcashAPI + +extension Client { + /// Fetches the core-mint balance for any owner account. No signature is + /// required — the server allows reading balance for any owner. + public func getBalance(owner: PublicKey) async throws -> TokenAmount { + try await withCheckedThrowingContinuation { c in + balanceService.getBalance(owner: owner) { c.resume(with: $0) } + } + } +} diff --git a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client.swift b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client.swift index 2f73965db..b18058f74 100644 --- a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client.swift +++ b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Client.swift @@ -27,6 +27,7 @@ public class Client: ObservableObject { internal let transactionService: TransactionService internal let currencyService: CurrencyService internal let messagingService: MessagingService + internal let balanceService: BalanceService // MARK: - Init - @@ -53,6 +54,7 @@ public class Client: ObservableObject { self.transactionService = TransactionService(client: client) self.currencyService = CurrencyService(client: client) self.messagingService = MessagingService(client: client) + self.balanceService = BalanceService(client: client) } deinit { diff --git a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift new file mode 100644 index 000000000..1338f7c07 --- /dev/null +++ b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift @@ -0,0 +1,72 @@ +// +// BalanceService.swift +// FlipcashCore +// + +import Foundation +import FlipcashAPI +import GRPCCore + +private let logger = Logger(label: "flipcash.balance-service") + +final class BalanceService: Sendable { + + private let service: Ocp_Balance_V1_Balance.Client + + init(client: GRPCClient) { + self.service = Ocp_Balance_V1_Balance.Client(wrapping: client) + } + + /// Fetches the core-mint balance for any owner account. Unlike every other + /// Payments API request, `GetBalanceRequest` carries no auth/signature field — + /// the server allows reading balance for any owner, not just the caller's own, + /// so this takes a bare `PublicKey` rather than a signing `KeyPair`. + func getBalance(owner: PublicKey, completion: @Sendable @escaping (Result) -> Void) { + logger.info("Fetching balance") + + let request = Ocp_Balance_V1_GetBalanceRequest.with { + $0.owner = owner.solanaAccountID + } + + Task { + do { + let response = try await service.getBalance(request, options: .unaryDefault) + let error = ErrorGetBalance(rawValue: response.result.rawValue) ?? .unknown + guard error == .ok else { + logger.error("Failed to fetch balance", metadata: ["error": "\(error)"]) + await MainActor.run { completion(.failure(error)) } + return + } + let balance = TokenAmount(quarks: response.coreMintValue, mint: .usdf) + await MainActor.run { completion(.success(balance)) } + } catch let error as RPCError { + await MainActor.run { completion(.failure(.from(transportError: error))) } + } catch { + await MainActor.run { completion(.failure(.unknown)) } + } + } + } +} + +// MARK: - Errors - + +public enum ErrorGetBalance: Int, Error, Equatable, Sendable { + case ok + case denied + case notFound + case unknown = -1 + case transportFailure = -2 + case cancelled = -3 + case rejected = -4 +} + +extension ErrorGetBalance: ServerError, TransportClassifiableError { + public var reportingLevel: ErrorReportingLevel { + switch self { + case .ok, .transportFailure: .suppressed + case .cancelled: .info + case .denied, .notFound: .info + case .unknown, .rejected: .error + } + } +} diff --git a/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift index aaf2e3e16..fc3349142 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift @@ -40,6 +40,7 @@ struct TransportClassificationTests { // MARK: - Registry (one line per TransportClassifiableError conformer) - @Test func errorFetchBalance() { assertClassifies(ErrorFetchBalance.self) } + @Test func errorGetBalance() { assertClassifies(ErrorGetBalance.self) } @Test func errorRegisterAccount() { assertClassifies(ErrorRegisterAccount.self) } @Test func errorLoginAccount() { assertClassifies(ErrorLoginAccount.self) } From f1b24557cd278598e31d4b63beeeb92934cd3292 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 26 Aug 2026 10:50:55 -0400 Subject: [PATCH 2/2] chore(deps): bump ocp-client-protocol to 0.2.0 Picks up the Balance service added upstream in ea6418c5. --- FlipcashAPI/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlipcashAPI/Package.swift b/FlipcashAPI/Package.swift index 8c52e70fd..75921f913 100644 --- a/FlipcashAPI/Package.swift +++ b/FlipcashAPI/Package.swift @@ -34,7 +34,7 @@ let contractDependencies: [Package.Dependency] = protoLocalRoot.map { root in .package(path: "\(root)/flipcash2-client-protocol"), ] } ?? [ - .package(url: "https://github.com/code-payments/ocp-client-protocol", exact: "0.1.0"), + .package(url: "https://github.com/code-payments/ocp-client-protocol", exact: "0.2.0"), .package(url: "https://github.com/code-payments/flipcash2-client-protocol", exact: "0.1.0"), ]