diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 8599d00d3..57ee06a11 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -679,6 +679,9 @@ Rate Unavailable Couldn\'t get a fresh rate. Please try again. + Amount Too Small + The amount you entered is too small to transfer\nPlease enter a larger amount + Settings Withdraw as USDC diff --git a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt index 8c7e7b8ba..508626051 100644 --- a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt +++ b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt @@ -316,11 +316,19 @@ internal class CashScreenViewModel @Inject constructor( token = token, balance = balance.underlyingTokenAmount, rate = rate, - ).getOrElse { + ).getOrElse { error -> dispatchEvent(Event.UpdateLoadingState(loading = false)) + val (title, message) = when (error) { + is ComputeVerifiedFiatError.AmountBelowMinimum -> { + R.string.error_title_amountTooSmall to R.string.error_description_amountTooSmall + } + else -> { + R.string.error_title_staleRates to R.string.error_description_staleRates + } + } BottomBarManager.showAlert( - title = resources.getString(R.string.error_title_staleRates), - message = resources.getString(R.string.error_description_staleRates), + title = resources.getString(title), + message = resources.getString(message), ) return@onEach } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculator.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculator.kt index d8f93955a..3227bef7d 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculator.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculator.kt @@ -114,6 +114,11 @@ internal class RealVerifiedFiatCalculator @Inject constructor( val underlyingTokenAmount = Fiat(quarks = quarks.toLong(), currencyCode = CurrencyCode.USD) val sellEstimate = Fiat.tokenBalance(quarks.toLong(), token, supply).convertingTo(rate) + + if (!sellEstimate.hasDisplayableValue) { + return Result.failure(ComputeVerifiedFiatError.AmountBelowMinimum()) + } + val fx = sellEstimate.decimalValue.toBigDecimal().divideWithHighPrecision(units).toDouble() if (trace) { 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 358f44ff6..7abef714a 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 @@ -382,4 +382,5 @@ sealed class ComputeVerifiedFiatError( class StaleRate : ComputeVerifiedFiatError("Reserve state unavailable or stale") data class ComputationFailed(override val cause: Throwable? = null) : ComputeVerifiedFiatError(message = cause?.message, cause = cause) + class AmountBelowMinimum : ComputeVerifiedFiatError("Amount is too small to transfer") } diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculatorTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculatorTest.kt index 6c977efee..5abf381bf 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculatorTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculatorTest.kt @@ -396,6 +396,32 @@ class RealVerifiedFiatCalculatorTest { // endregion + // region sub-minimum amount + + @Test + fun `returns AmountBelowMinimum when sell estimate has no displayable value`() = runTest { + // Simulates the Bugsnag scenario: a low-supply custom token where the + // bonding curve produces a native amount below the currency's smallest + // displayable unit (e.g. ₦0.003 rounds to ₦0.00 for NGN). + val supply = 1_000_000_000_000L + val token = bondingCurveToken(supply = supply) + stubVerifiedState(CurrencyCode.USD, testMint, supply) + + // Use a sub-cent amount ($0.000001 = 1 quark) so the curve succeeds + // but the sell estimate is below the smallest displayable USD unit ($0.01). + val result = calculator.compute( + amount = Fiat(quarks = 1, currencyCode = CurrencyCode.USD), + token = token, + rate = Rate.oneToOne, + trace = false, + ) + + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + } + + // endregion + // region helpers private fun usdfToken(): Token = MintMetadata(