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 @@ -66,7 +66,7 @@ protovalidate-kt = "0.1.1"
# Generated client SDKs for the two backend contracts. Separate versions on purpose: the
# protos are independent (flipcash2 does not import ocp), so the packages move on their own
# cadence and there is nothing to keep aligned.
ocp-client-protocol = "0.1.0"
ocp-client-protocol = "0.2.0"
flipcash2-client-protocol = "0.1.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
@@ -0,0 +1,24 @@
package com.getcode.opencode.controllers

import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.repositories.BalanceRepository
import com.getcode.solana.keys.PublicKey
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class BalanceController @Inject constructor(
private val balanceRepository: BalanceRepository,
) {
/**
* Returns the owner's core-mint (USDF) balance.
*
* 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.
*/
suspend fun getBalance(owner: PublicKey): Result<Fiat> {
return balanceRepository.getBalance(owner)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.getcode.opencode.internal.annotations.OpenCodeManagedChannel
import com.getcode.opencode.internal.annotations.OpenCodeManagedStreamingChannel
import com.getcode.opencode.internal.annotations.OpenCodeProtocol
import com.getcode.opencode.internal.domain.repositories.InternalAccountRepository
import com.getcode.opencode.internal.domain.repositories.InternalBalanceRepository
import com.getcode.opencode.internal.domain.repositories.InternalCurrencyRepository
import com.getcode.opencode.internal.domain.repositories.InternalEventRepository
import com.getcode.opencode.internal.domain.repositories.InternalMessagingRepository
Expand All @@ -23,12 +24,14 @@ import com.getcode.opencode.internal.exchange.RealVerifiedFiatCalculator
import com.getcode.opencode.internal.manager.VerifiedProtoManager
import com.getcode.opencode.internal.network.pollers.SwapPoller
import com.getcode.opencode.internal.network.services.AccountService
import com.getcode.opencode.internal.network.services.BalanceService
import com.getcode.opencode.internal.network.services.CurrencyService
import com.getcode.opencode.internal.network.services.MessagingService
import com.getcode.opencode.internal.network.services.SwapService
import com.getcode.opencode.internal.network.services.TransactionService
import com.getcode.opencode.providers.SessionListener
import com.getcode.opencode.repositories.AccountRepository
import com.getcode.opencode.repositories.BalanceRepository
import com.getcode.opencode.repositories.CurrencyRepository
import com.getcode.opencode.repositories.EventRepository
import com.getcode.opencode.repositories.MessagingRepository
Expand Down Expand Up @@ -179,6 +182,13 @@ object OpenCodeModule {
internal fun providesCurrencyRepository(
service: CurrencyService
): CurrencyRepository = InternalCurrencyRepository(service)

@Provides
@Singleton
internal fun providesBalanceRepository(
service: BalanceService
): BalanceRepository = InternalBalanceRepository(service)

@Provides
@Singleton
internal fun providesEventBus(): ChannelEventBus = ChannelEventBus()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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.repositories.BalanceRepository
import com.getcode.solana.keys.PublicKey
import com.getcode.utils.ErrorUtils
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)
.onFailure { error ->
if (error !is GetBalanceError.NotFound && error !is GetBalanceError.Denied) {
ErrorUtils.handleError(error)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.getcode.opencode.internal.network.api

import com.codeinc.opencode.gen.balance.v1.BalanceGrpcKt
import com.codeinc.opencode.gen.balance.v1.OcpBalanceService
import com.codeinc.opencode.gen.balance.v1.validate
import com.getcode.opencode.internal.annotations.OpenCodeManagedChannel
import com.getcode.opencode.internal.network.core.GrpcApi
import com.getcode.opencode.internal.network.extensions.asSolanaAccountId
import com.getcode.solana.keys.PublicKey
import dev.bmcreations.protovalidate.orThrow
import io.grpc.ManagedChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class BalanceApi @Inject constructor(
@OpenCodeManagedChannel
managedChannel: ManagedChannel,
) : GrpcApi(managedChannel) {

private val api = BalanceGrpcKt.BalanceCoroutineStub(managedChannel)
.withWaitForReady()

/**
* Returns balance data for any owner account.
*
* 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
* 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]
*/
suspend fun getBalance(
owner: PublicKey,
): OcpBalanceService.GetBalanceResponse {
val request = OcpBalanceService.GetBalanceRequest.newBuilder()
.setOwner(owner.asSolanaAccountId())
.build()

request.validate().orThrow()

return withContext(Dispatchers.IO) {
api.getBalance(request)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.getcode.opencode.internal.network.services

import com.codeinc.opencode.gen.balance.v1.OcpBalanceService
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.utils.toValidationOrElse
import com.getcode.solana.keys.PublicKey
import javax.inject.Inject

internal class BalanceService @Inject constructor(
private val api: BalanceApi,
) {
suspend fun getBalance(owner: PublicKey): Result<Fiat> {
return runCatching {
api.getBalance(owner)
}.foldWithSuppression(
onSuccess = { response ->
when (response.result) {
OcpBalanceService.GetBalanceResponse.Result.OK -> Result.success(
Fiat(quarks = response.coreMintValue, currencyCode = CurrencyCode.USD)
)
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())
}
},
onFailure = { cause ->
Result.failure(cause.toValidationOrElse { GetBalanceError.Other(cause = it) })
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ sealed class GetRatesError(
data class Other(override val cause: Throwable? = null) : GetRatesError(message = cause?.message, cause = cause), NotifiableError
}

sealed class GetBalanceError(
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
}

sealed class GetMintsError(
override val message: String? = null,
override val cause: Throwable? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.getcode.opencode.repositories

import com.getcode.opencode.model.financial.Fiat
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.
*
* Unauthenticated — no signing key is required, only the account's address.
*/
suspend fun getBalance(owner: PublicKey): Result<Fiat>
}
Loading