diff --git a/FlipcashAPI/Package.swift b/FlipcashAPI/Package.swift index 2c3f49092..ad6b01c4b 100644 --- a/FlipcashAPI/Package.swift +++ b/FlipcashAPI/Package.swift @@ -55,7 +55,7 @@ enum ContractPackage: String, CaseIterable { /// The pinned version consumed when this package isn't building against a local checkout. var version: Version { switch self { - case .ocp: return "0.3.0" + case .ocp: return "0.4.0" case .flipcash2: return "0.6.0" } } diff --git a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift index 1338f7c07..35ebb6247 100644 --- a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift +++ b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/BalanceService.swift @@ -17,27 +17,38 @@ final class BalanceService: Sendable { 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`. + /// Fetches the core-mint balance for a single owner account. Unlike every + /// other Payments API request, `GetBalancesRequest` 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`. + /// + /// The underlying RPC is batched — `GetBalances` takes a list of owners and + /// an optional mint filter, returning an owner → mint → value map — but + /// nothing in the app currently needs more than one owner's balance at a + /// time, so this wrapper stays single-owner-shaped and reads the one entry + /// it asked for back out of the map. func getBalance(owner: PublicKey, completion: @Sendable @escaping (Result) -> Void) { logger.info("Fetching balance") - let request = Ocp_Balance_V1_GetBalanceRequest.with { - $0.owner = owner.solanaAccountID + let request = Ocp_Balance_V1_GetBalancesRequest.with { + $0.owners = [owner.solanaAccountID] } Task { do { - let response = try await service.getBalance(request, options: .unaryDefault) + let response = try await service.getBalances(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) + // An owner absent from the map simply has no balances — that's + // zero, not an error. (The old response's NOT_FOUND result case + // is gone; this replaces it.) + let quarks = response.balancesByOwner[owner.base58]?.coreMintValue ?? 0 + let balance = TokenAmount(quarks: quarks, mint: .usdf) await MainActor.run { completion(.success(balance)) } } catch let error as RPCError { await MainActor.run { completion(.failure(.from(transportError: error))) } @@ -53,7 +64,6 @@ final class BalanceService: Sendable { public enum ErrorGetBalance: Int, Error, Equatable, Sendable { case ok case denied - case notFound case unknown = -1 case transportFailure = -2 case cancelled = -3 @@ -65,7 +75,7 @@ extension ErrorGetBalance: ServerError, TransportClassifiableError { switch self { case .ok, .transportFailure: .suppressed case .cancelled: .info - case .denied, .notFound: .info + case .denied: .info case .unknown, .rejected: .error } }