Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Fiat> {
return balanceRepository.getBalance(owner)
suspend fun getBalances(
owners: List<PublicKey>,
mints: List<PublicKey> = emptyList(),
): Result<List<OwnerBalance>> {
return balanceRepository.getBalances(owners, mints)
}
}
Original file line number Diff line number Diff line change
@@ -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<OcpBalanceService.OwnerBalance, OwnerBalance> {
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
)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Fiat> =
service.getBalance(owner)
override suspend fun getBalances(
owners: List<PublicKey>,
mints: List<PublicKey>,
): Result<List<OwnerBalance>> =
service.getBalances(owners, mints)
.onFailure { error ->
if (error !is GetBalanceError.NotFound && error !is GetBalanceError.Denied) {
if (error !is GetBalancesError.Denied) {
ErrorUtils.handleError(error)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PublicKey>,
mints: List<PublicKey> = 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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Fiat> {
suspend fun getBalances(
owners: List<PublicKey>,
mints: List<PublicKey> = emptyList(),
): Result<List<OwnerBalance>> {
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) })
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Mint, Fiat> = emptyMap(),
)
Original file line number Diff line number Diff line change
@@ -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<Fiat>
suspend fun getBalances(
owners: List<PublicKey>,
mints: List<PublicKey> = emptyList(),
): Result<List<OwnerBalance>>
}
Loading