Skip to content

Commit f07170d

Browse files
authored
feat(opencode): move to the batched GetBalances RPC (#1458)
1 parent b7bab35 commit f07170d

9 files changed

Lines changed: 112 additions & 48 deletions

File tree

‎gradle/libs.versions.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ protovalidate-kt = "0.1.2"
6868
# cadence and there is nothing to keep aligned.
6969
# 0.3.0 is the first release of either package to ship R8 keep rules for its generated
7070
# messages, which is what lets proguard-rules.pro drop its own.
71-
ocp-client-protocol = "0.3.0"
71+
ocp-client-protocol = "0.4.0"
7272
flipcash2-client-protocol = "0.5.0"
7373

7474
# The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.getcode.opencode.controllers
22

3-
import com.getcode.opencode.model.financial.Fiat
3+
import com.getcode.opencode.model.financial.OwnerBalance
44
import com.getcode.opencode.repositories.BalanceRepository
55
import com.getcode.solana.keys.PublicKey
66
import javax.inject.Inject
@@ -11,14 +11,18 @@ class BalanceController @Inject constructor(
1111
private val balanceRepository: BalanceRepository,
1212
) {
1313
/**
14-
* Returns the owner's core-mint (USDF) balance.
14+
* Returns balance data for the given owner accounts, optionally filtered to a
15+
* set of mints.
1516
*
1617
* Unlike the rest of this package's controllers, this does not take an
17-
* `AccountCluster` — the underlying RPC is unauthenticated and unsigned, so a
18-
* bare [PublicKey] is all that's needed, and this can resolve balance for any
19-
* owner account, not just the current user's.
18+
* `AccountCluster` — the underlying RPC is unauthenticated and unsigned, so
19+
* bare [PublicKey]s are all that's needed, and this can resolve balances for
20+
* any owner accounts, not just the current user's.
2021
*/
21-
suspend fun getBalance(owner: PublicKey): Result<Fiat> {
22-
return balanceRepository.getBalance(owner)
22+
suspend fun getBalances(
23+
owners: List<PublicKey>,
24+
mints: List<PublicKey> = emptyList(),
25+
): Result<List<OwnerBalance>> {
26+
return balanceRepository.getBalances(owners, mints)
2327
}
2428
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.getcode.opencode.internal.domain.mapping
2+
3+
import com.codeinc.opencode.gen.balance.v1.OcpBalanceService
4+
import com.getcode.opencode.internal.network.extensions.toMint
5+
import com.getcode.opencode.internal.network.extensions.toPublicKey
6+
import com.getcode.opencode.mapper.Mapper
7+
import com.getcode.opencode.model.financial.CurrencyCode
8+
import com.getcode.opencode.model.financial.Fiat
9+
import com.getcode.opencode.model.financial.OwnerBalance
10+
import javax.inject.Inject
11+
12+
internal class OwnerBalanceMapper @Inject constructor() :
13+
Mapper<OcpBalanceService.OwnerBalance, OwnerBalance> {
14+
override fun map(from: OcpBalanceService.OwnerBalance): OwnerBalance {
15+
return OwnerBalance(
16+
owner = from.owner.toPublicKey(),
17+
coreMintValue = Fiat(quarks = from.coreMintValue, currencyCode = CurrencyCode.USD),
18+
balancesByMint = from.balancesByMintMap.values.associate { mintBalance ->
19+
mintBalance.mint.toMint() to Fiat(
20+
quarks = mintBalance.coreMintValue,
21+
currencyCode = CurrencyCode.USD
22+
)
23+
}
24+
)
25+
}
26+
}

‎services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalBalanceRepository.kt‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.getcode.opencode.internal.domain.repositories
22

33
import com.getcode.opencode.internal.network.services.BalanceService
4-
import com.getcode.opencode.model.core.errors.GetBalanceError
5-
import com.getcode.opencode.model.financial.Fiat
4+
import com.getcode.opencode.model.core.errors.GetBalancesError
5+
import com.getcode.opencode.model.financial.OwnerBalance
66
import com.getcode.opencode.repositories.BalanceRepository
77
import com.getcode.solana.keys.PublicKey
88
import com.getcode.utils.ErrorUtils
@@ -11,10 +11,13 @@ import javax.inject.Inject
1111
internal class InternalBalanceRepository @Inject constructor(
1212
private val service: BalanceService,
1313
) : BalanceRepository {
14-
override suspend fun getBalance(owner: PublicKey): Result<Fiat> =
15-
service.getBalance(owner)
14+
override suspend fun getBalances(
15+
owners: List<PublicKey>,
16+
mints: List<PublicKey>,
17+
): Result<List<OwnerBalance>> =
18+
service.getBalances(owners, mints)
1619
.onFailure { error ->
17-
if (error !is GetBalanceError.NotFound && error !is GetBalanceError.Denied) {
20+
if (error !is GetBalancesError.Denied) {
1821
ErrorUtils.handleError(error)
1922
}
2023
}

‎services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,31 @@ internal class BalanceApi @Inject constructor(
2424
.withWaitForReady()
2525

2626
/**
27-
* Returns balance data for any owner account.
27+
* Returns balance data for a set of owner accounts, optionally filtered to a
28+
* set of mints.
2829
*
2930
* Unlike every other OpenCode endpoint, this RPC carries no auth/signature field —
30-
* it is intentionally unauthenticated so it can resolve the balance for any owner
31+
* it is intentionally unauthenticated so it can resolve balances for any owner
3132
* account address, not just the caller's own. Do not sign this request.
3233
*
33-
* @param owner The owner account to fetch balance data for.
34-
* @return The [OcpBalanceService.GetBalanceResponse]
34+
* @param owners The owner accounts to fetch balance data for (min 1, max 1024).
35+
* @param mints Optional filter to limit the response to balances for these mints.
36+
* When empty, balances for all mints held by each owner are returned.
37+
* @return The [OcpBalanceService.GetBalancesResponse]
3538
*/
36-
suspend fun getBalance(
37-
owner: PublicKey,
38-
): OcpBalanceService.GetBalanceResponse {
39-
val request = OcpBalanceService.GetBalanceRequest.newBuilder()
40-
.setOwner(owner.asSolanaAccountId())
39+
suspend fun getBalances(
40+
owners: List<PublicKey>,
41+
mints: List<PublicKey> = emptyList(),
42+
): OcpBalanceService.GetBalancesResponse {
43+
val request = OcpBalanceService.GetBalancesRequest.newBuilder()
44+
.addAllOwners(owners.map { it.asSolanaAccountId() })
45+
.addAllMints(mints.map { it.asSolanaAccountId() })
4146
.build()
4247

4348
request.validate().orThrow()
4449

4550
return withContext(Dispatchers.IO) {
46-
api.getBalance(request)
51+
api.getBalances(request)
4752
}
4853
}
4954
}

‎services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/BalanceService.kt‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
11
package com.getcode.opencode.internal.network.services
22

33
import com.codeinc.opencode.gen.balance.v1.OcpBalanceService
4+
import com.getcode.opencode.internal.domain.mapping.OwnerBalanceMapper
45
import com.getcode.opencode.internal.network.api.BalanceApi
56
import com.getcode.opencode.internal.network.extensions.foldWithSuppression
6-
import com.getcode.opencode.model.core.errors.GetBalanceError
7-
import com.getcode.opencode.model.financial.CurrencyCode
8-
import com.getcode.opencode.model.financial.Fiat
7+
import com.getcode.opencode.model.core.errors.GetBalancesError
8+
import com.getcode.opencode.model.financial.OwnerBalance
99
import com.getcode.opencode.utils.toValidationOrElse
1010
import com.getcode.solana.keys.PublicKey
1111
import javax.inject.Inject
1212

1313
internal class BalanceService @Inject constructor(
1414
private val api: BalanceApi,
15+
private val ownerBalanceMapper: OwnerBalanceMapper,
1516
) {
16-
suspend fun getBalance(owner: PublicKey): Result<Fiat> {
17+
suspend fun getBalances(
18+
owners: List<PublicKey>,
19+
mints: List<PublicKey> = emptyList(),
20+
): Result<List<OwnerBalance>> {
1721
return runCatching {
18-
api.getBalance(owner)
22+
api.getBalances(owners, mints)
1923
}.foldWithSuppression(
2024
onSuccess = { response ->
2125
when (response.result) {
22-
OcpBalanceService.GetBalanceResponse.Result.OK -> Result.success(
23-
Fiat(quarks = response.coreMintValue, currencyCode = CurrencyCode.USD)
26+
OcpBalanceService.GetBalancesResponse.Result.OK -> Result.success(
27+
response.balancesByOwnerMap.values.map { ownerBalanceMapper.map(it) }
2428
)
25-
OcpBalanceService.GetBalanceResponse.Result.DENIED -> Result.failure(
26-
GetBalanceError.Denied())
27-
OcpBalanceService.GetBalanceResponse.Result.NOT_FOUND -> Result.failure(
28-
GetBalanceError.NotFound())
29-
OcpBalanceService.GetBalanceResponse.Result.UNRECOGNIZED -> Result.failure(
30-
GetBalanceError.Unrecognized())
31-
else -> Result.failure(GetBalanceError.Other())
29+
OcpBalanceService.GetBalancesResponse.Result.DENIED -> Result.failure(
30+
GetBalancesError.Denied())
31+
OcpBalanceService.GetBalancesResponse.Result.UNRECOGNIZED -> Result.failure(
32+
GetBalancesError.Unrecognized())
33+
else -> Result.failure(GetBalancesError.Other())
3234
}
3335
},
3436
onFailure = { cause ->
35-
Result.failure(cause.toValidationOrElse { GetBalanceError.Other(cause = it) })
37+
Result.failure(cause.toValidationOrElse { GetBalancesError.Other(cause = it) })
3638
}
3739
)
3840
}

‎services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,13 @@ sealed class GetRatesError(
6161
data class Other(override val cause: Throwable? = null) : GetRatesError(message = cause?.message, cause = cause), NotifiableError
6262
}
6363

64-
sealed class GetBalanceError(
64+
sealed class GetBalancesError(
6565
override val message: String? = null,
6666
override val cause: Throwable? = null
6767
) : CodeServerError(message, cause) {
68-
class Denied : GetBalanceError("Denied")
69-
class NotFound : GetBalanceError("Not found")
70-
class Unrecognized : GetBalanceError("Unrecognized"), NotifiableError
71-
data class Other(override val cause: Throwable? = null) : GetBalanceError(message = cause?.message, cause = cause), NotifiableError
68+
class Denied : GetBalancesError("Denied")
69+
class Unrecognized : GetBalancesError("Unrecognized"), NotifiableError
70+
data class Other(override val cause: Throwable? = null) : GetBalancesError(message = cause?.message, cause = cause), NotifiableError
7271
}
7372

7473
sealed class GetMintsError(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.getcode.opencode.model.financial
2+
3+
import com.getcode.solana.keys.Mint
4+
import com.getcode.solana.keys.PublicKey
5+
6+
/**
7+
* Balance data for a single owner account, as returned by `Balance.GetBalances`.
8+
*
9+
* [coreMintValue] is the owner's total across all mints, denominated in the core
10+
* mint. [balancesByMint] breaks that total down per mint; a mint the owner holds
11+
* no balance in is simply absent from the map.
12+
*/
13+
data class OwnerBalance(
14+
val owner: PublicKey,
15+
val coreMintValue: Fiat,
16+
val balancesByMint: Map<Mint, Fiat> = emptyMap(),
17+
)
Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package com.getcode.opencode.repositories
22

3-
import com.getcode.opencode.model.financial.Fiat
3+
import com.getcode.opencode.model.financial.OwnerBalance
44
import com.getcode.solana.keys.PublicKey
55

66
interface BalanceRepository {
77
/**
8-
* Returns the owner's core-mint (USDF) balance. The response carries a raw quark count
9-
* and no currency code; USDF is 6 decimals, which is the unit [Fiat] already counts in.
8+
* Returns balance data for the given owner accounts, optionally filtered to a
9+
* set of mints. Each result entry carries the owner's core-mint (USDF) total
10+
* plus a per-mint breakdown; USDF is 6 decimals, which is the unit `Fiat`
11+
* already counts in.
1012
*
11-
* Unauthenticated — no signing key is required, only the account's address.
13+
* An owner with no balance for the requested mints is simply absent from the
14+
* result rather than represented as an error.
15+
*
16+
* Unauthenticated — no signing key is required, only the accounts' addresses.
1217
*/
13-
suspend fun getBalance(owner: PublicKey): Result<Fiat>
18+
suspend fun getBalances(
19+
owners: List<PublicKey>,
20+
mints: List<PublicKey> = emptyList(),
21+
): Result<List<OwnerBalance>>
1422
}

0 commit comments

Comments
 (0)