From 85b690d6fe54d7655e7b7fd25f6fdba5da05ceb1 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 14 Sep 2026 14:14:26 -0400 Subject: [PATCH 1/2] feat(opencode): scaffold GetBalances for the removed GetBalance RPC ocp-protobuf-api replaced Balance.GetBalance with GetBalances (owners list, optional mint filter, owner -> mint -> quarks map). Update BalanceApi, BalanceService, BalanceRepository, and BalanceController through the same chain, add an OwnerBalance domain type plus mapper to carry the two-level result, and drop the now-impossible NOT_FOUND handling in favor of the absent-map-entry path. Compiles against the local ocp-client-protocol checkout via protoLocalRoot. No feature currently calls BalanceController, so this is scaffolding only. --- .../opencode/controllers/BalanceController.kt | 18 +++++++---- .../domain/mapping/OwnerBalanceMapper.kt | 26 +++++++++++++++ .../repositories/InternalBalanceRepository.kt | 13 +++++--- .../internal/network/api/BalanceApi.kt | 25 +++++++++------ .../network/services/BalanceService.kt | 32 ++++++++++--------- .../opencode/model/core/errors/Errors.kt | 9 +++--- .../opencode/model/financial/OwnerBalance.kt | 17 ++++++++++ .../repositories/BalanceRepository.kt | 18 ++++++++--- 8 files changed, 111 insertions(+), 47 deletions(-) create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/mapping/OwnerBalanceMapper.kt create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/OwnerBalance.kt diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/BalanceController.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/BalanceController.kt index 87988dead..99a9d64fd 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/BalanceController.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/BalanceController.kt @@ -1,6 +1,6 @@ package com.getcode.opencode.controllers -import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.financial.OwnerBalance import com.getcode.opencode.repositories.BalanceRepository import com.getcode.solana.keys.PublicKey import javax.inject.Inject @@ -11,14 +11,18 @@ class BalanceController @Inject constructor( private val balanceRepository: BalanceRepository, ) { /** - * Returns the owner's core-mint (USDF) balance. + * Returns balance data for the given owner accounts, optionally filtered to a + * set of mints. * * Unlike the rest of this package's controllers, this does not take an - * `AccountCluster` — the underlying RPC is unauthenticated and unsigned, so a - * bare [PublicKey] is all that's needed, and this can resolve balance for any - * owner account, not just the current user's. + * `AccountCluster` — the underlying RPC is unauthenticated and unsigned, so + * bare [PublicKey]s are all that's needed, and this can resolve balances for + * any owner accounts, not just the current user's. */ - suspend fun getBalance(owner: PublicKey): Result { - return balanceRepository.getBalance(owner) + suspend fun getBalances( + owners: List, + mints: List = emptyList(), + ): Result> { + return balanceRepository.getBalances(owners, mints) } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/mapping/OwnerBalanceMapper.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/mapping/OwnerBalanceMapper.kt new file mode 100644 index 000000000..8be8e69df --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/mapping/OwnerBalanceMapper.kt @@ -0,0 +1,26 @@ +package com.getcode.opencode.internal.domain.mapping + +import com.codeinc.opencode.gen.balance.v1.OcpBalanceService +import com.getcode.opencode.internal.network.extensions.toMint +import com.getcode.opencode.internal.network.extensions.toPublicKey +import com.getcode.opencode.mapper.Mapper +import com.getcode.opencode.model.financial.CurrencyCode +import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.financial.OwnerBalance +import javax.inject.Inject + +internal class OwnerBalanceMapper @Inject constructor() : + Mapper { + override fun map(from: OcpBalanceService.OwnerBalance): OwnerBalance { + return OwnerBalance( + owner = from.owner.toPublicKey(), + coreMintValue = Fiat(quarks = from.coreMintValue, currencyCode = CurrencyCode.USD), + balancesByMint = from.balancesByMintMap.values.associate { mintBalance -> + mintBalance.mint.toMint() to Fiat( + quarks = mintBalance.coreMintValue, + currencyCode = CurrencyCode.USD + ) + } + ) + } +} diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalBalanceRepository.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalBalanceRepository.kt index 118234719..43f026d89 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalBalanceRepository.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalBalanceRepository.kt @@ -1,8 +1,8 @@ package com.getcode.opencode.internal.domain.repositories import com.getcode.opencode.internal.network.services.BalanceService -import com.getcode.opencode.model.core.errors.GetBalanceError -import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.core.errors.GetBalancesError +import com.getcode.opencode.model.financial.OwnerBalance import com.getcode.opencode.repositories.BalanceRepository import com.getcode.solana.keys.PublicKey import com.getcode.utils.ErrorUtils @@ -11,10 +11,13 @@ import javax.inject.Inject internal class InternalBalanceRepository @Inject constructor( private val service: BalanceService, ) : BalanceRepository { - override suspend fun getBalance(owner: PublicKey): Result = - service.getBalance(owner) + override suspend fun getBalances( + owners: List, + mints: List, + ): Result> = + service.getBalances(owners, mints) .onFailure { error -> - if (error !is GetBalanceError.NotFound && error !is GetBalanceError.Denied) { + if (error !is GetBalancesError.Denied) { ErrorUtils.handleError(error) } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt index f73e0d279..a832dca40 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt @@ -24,26 +24,31 @@ internal class BalanceApi @Inject constructor( .withWaitForReady() /** - * Returns balance data for any owner account. + * Returns balance data for a set of owner accounts, optionally filtered to a + * set of mints. * * Unlike every other OpenCode endpoint, this RPC carries no auth/signature field — - * it is intentionally unauthenticated so it can resolve the balance for any owner + * it is intentionally unauthenticated so it can resolve balances for any owner * account address, not just the caller's own. Do not sign this request. * - * @param owner The owner account to fetch balance data for. - * @return The [OcpBalanceService.GetBalanceResponse] + * @param owners The owner accounts to fetch balance data for (min 1, max 1024). + * @param mints Optional filter to limit the response to balances for these mints. + * When empty, balances for all mints held by each owner are returned. + * @return The [OcpBalanceService.GetBalancesResponse] */ - suspend fun getBalance( - owner: PublicKey, - ): OcpBalanceService.GetBalanceResponse { - val request = OcpBalanceService.GetBalanceRequest.newBuilder() - .setOwner(owner.asSolanaAccountId()) + suspend fun getBalances( + owners: List, + mints: List = emptyList(), + ): OcpBalanceService.GetBalancesResponse { + val request = OcpBalanceService.GetBalancesRequest.newBuilder() + .addAllOwners(owners.map { it.asSolanaAccountId() }) + .addAllMints(mints.map { it.asSolanaAccountId() }) .build() request.validate().orThrow() return withContext(Dispatchers.IO) { - api.getBalance(request) + api.getBalances(request) } } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/BalanceService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/BalanceService.kt index ce8cd82af..582759755 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/BalanceService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/BalanceService.kt @@ -1,38 +1,40 @@ package com.getcode.opencode.internal.network.services import com.codeinc.opencode.gen.balance.v1.OcpBalanceService +import com.getcode.opencode.internal.domain.mapping.OwnerBalanceMapper import com.getcode.opencode.internal.network.api.BalanceApi import com.getcode.opencode.internal.network.extensions.foldWithSuppression -import com.getcode.opencode.model.core.errors.GetBalanceError -import com.getcode.opencode.model.financial.CurrencyCode -import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.core.errors.GetBalancesError +import com.getcode.opencode.model.financial.OwnerBalance import com.getcode.opencode.utils.toValidationOrElse import com.getcode.solana.keys.PublicKey import javax.inject.Inject internal class BalanceService @Inject constructor( private val api: BalanceApi, + private val ownerBalanceMapper: OwnerBalanceMapper, ) { - suspend fun getBalance(owner: PublicKey): Result { + suspend fun getBalances( + owners: List, + mints: List = emptyList(), + ): Result> { return runCatching { - api.getBalance(owner) + api.getBalances(owners, mints) }.foldWithSuppression( onSuccess = { response -> when (response.result) { - OcpBalanceService.GetBalanceResponse.Result.OK -> Result.success( - Fiat(quarks = response.coreMintValue, currencyCode = CurrencyCode.USD) + OcpBalanceService.GetBalancesResponse.Result.OK -> Result.success( + response.balancesByOwnerMap.values.map { ownerBalanceMapper.map(it) } ) - OcpBalanceService.GetBalanceResponse.Result.DENIED -> Result.failure( - GetBalanceError.Denied()) - OcpBalanceService.GetBalanceResponse.Result.NOT_FOUND -> Result.failure( - GetBalanceError.NotFound()) - OcpBalanceService.GetBalanceResponse.Result.UNRECOGNIZED -> Result.failure( - GetBalanceError.Unrecognized()) - else -> Result.failure(GetBalanceError.Other()) + OcpBalanceService.GetBalancesResponse.Result.DENIED -> Result.failure( + GetBalancesError.Denied()) + OcpBalanceService.GetBalancesResponse.Result.UNRECOGNIZED -> Result.failure( + GetBalancesError.Unrecognized()) + else -> Result.failure(GetBalancesError.Other()) } }, onFailure = { cause -> - Result.failure(cause.toValidationOrElse { GetBalanceError.Other(cause = it) }) + Result.failure(cause.toValidationOrElse { GetBalancesError.Other(cause = it) }) } ) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt index 2b3e84eae..8e327645a 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt @@ -61,14 +61,13 @@ sealed class GetRatesError( data class Other(override val cause: Throwable? = null) : GetRatesError(message = cause?.message, cause = cause), NotifiableError } -sealed class GetBalanceError( +sealed class GetBalancesError( override val message: String? = null, override val cause: Throwable? = null ) : CodeServerError(message, cause) { - class Denied : GetBalanceError("Denied") - class NotFound : GetBalanceError("Not found") - class Unrecognized : GetBalanceError("Unrecognized"), NotifiableError - data class Other(override val cause: Throwable? = null) : GetBalanceError(message = cause?.message, cause = cause), NotifiableError + class Denied : GetBalancesError("Denied") + class Unrecognized : GetBalancesError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : GetBalancesError(message = cause?.message, cause = cause), NotifiableError } sealed class GetMintsError( diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/OwnerBalance.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/OwnerBalance.kt new file mode 100644 index 000000000..82cb0eefa --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/OwnerBalance.kt @@ -0,0 +1,17 @@ +package com.getcode.opencode.model.financial + +import com.getcode.solana.keys.Mint +import com.getcode.solana.keys.PublicKey + +/** + * Balance data for a single owner account, as returned by `Balance.GetBalances`. + * + * [coreMintValue] is the owner's total across all mints, denominated in the core + * mint. [balancesByMint] breaks that total down per mint; a mint the owner holds + * no balance in is simply absent from the map. + */ +data class OwnerBalance( + val owner: PublicKey, + val coreMintValue: Fiat, + val balancesByMint: Map = emptyMap(), +) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/BalanceRepository.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/BalanceRepository.kt index 1be3d04a2..00dcf1cdd 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/BalanceRepository.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/BalanceRepository.kt @@ -1,14 +1,22 @@ package com.getcode.opencode.repositories -import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.financial.OwnerBalance import com.getcode.solana.keys.PublicKey interface BalanceRepository { /** - * Returns the owner's core-mint (USDF) balance. The response carries a raw quark count - * and no currency code; USDF is 6 decimals, which is the unit [Fiat] already counts in. + * Returns balance data for the given owner accounts, optionally filtered to a + * set of mints. Each result entry carries the owner's core-mint (USDF) total + * plus a per-mint breakdown; USDF is 6 decimals, which is the unit `Fiat` + * already counts in. * - * Unauthenticated — no signing key is required, only the account's address. + * An owner with no balance for the requested mints is simply absent from the + * result rather than represented as an error. + * + * Unauthenticated — no signing key is required, only the accounts' addresses. */ - suspend fun getBalance(owner: PublicKey): Result + suspend fun getBalances( + owners: List, + mints: List = emptyList(), + ): Result> } From 20f8a68c25230ce9b4c809dd99348a8ccc47a3fa Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 14 Sep 2026 14:43:58 -0400 Subject: [PATCH 2/2] build(deps): pin ocp-client-protocol 0.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea824bebb..54b484488 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ protovalidate-kt = "0.1.2" # cadence and there is nothing to keep aligned. # 0.3.0 is the first release of either package to ship R8 keep rules for its generated # messages, which is what lets proguard-rules.pro drop its own. -ocp-client-protocol = "0.3.0" +ocp-client-protocol = "0.4.0" flipcash2-client-protocol = "0.5.0" # The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's