Skip to content

Commit c2b60db

Browse files
committed
feat(exchange): return Result<VerifiedFiat> from VerifiedFiatCalculator.compute
Change compute() to return Result<VerifiedFiat> with a sealed ComputeVerifiedFiatError type so callers handle failures explicitly instead of silently falling back to stale data or crashing on Estimator.valueExchangeAsQuarks failures. - Add ComputeVerifiedFiatError.StaleRate and .ComputationFailed - USDF path always succeeds; non-USDF with null verifiedState returns StaleRate - Replace .getOrThrow() on Estimator result with early-return failure - Update all 11 call sites across 6 files with proper error handling - Update unit tests to match new Result return type Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 43a6d14 commit c2b60db

11 files changed

Lines changed: 191 additions & 77 deletions

File tree

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,4 +606,7 @@
606606

607607
<string name="error_title_noInternet">Something Went Wrong</string>
608608
<string name="error_description_noInternet">Please check your internet connection or try again later.</string>
609+
610+
<string name="error_title_staleRates">Rate Unavailable</string>
611+
<string name="error_description_staleRates">Couldn\'t get a fresh rate. Please try again.</string>
609612
</resources>

apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.getcode.manager.BottomBarManager
1212
import com.getcode.opencode.controllers.TransactionOperations
1313
import com.getcode.opencode.exchange.Exchange
1414
import com.getcode.opencode.exchange.VerifiedFiatCalculator
15+
import com.getcode.opencode.model.core.errors.ComputeVerifiedFiatError
1516
import com.getcode.opencode.model.financial.Currency
1617
import com.getcode.opencode.model.financial.CurrencyCode
1718
import com.getcode.opencode.model.financial.Fiat
@@ -146,10 +147,15 @@ internal class CashScreenViewModel @Inject constructor(
146147
amount = Fiat(amount, rate.currency),
147148
token = token,
148149
rate = rate,
149-
).localFiat
150-
151-
val neededAmount = amountFiat.nativeAmount - tokenBalance
152-
println("entered amount ${amountFiat.nativeAmount}, tokenbalace=$tokenBalance, needed=$neededAmount")
150+
).getOrElse {
151+
BottomBarManager.showAlert(
152+
title = resources.getString(R.string.error_title_staleRates),
153+
message = resources.getString(R.string.error_description_staleRates),
154+
)
155+
return@launch
156+
}
157+
158+
val neededAmount = amountFiat.localFiat.nativeAmount - tokenBalance
153159
dispatchEvent(Event.AddCashToWallet(neededAmount))
154160
}
155161
},
@@ -309,7 +315,14 @@ internal class CashScreenViewModel @Inject constructor(
309315
token = token,
310316
balance = balance.underlyingTokenAmount,
311317
rate = rate,
312-
)
318+
).getOrElse {
319+
dispatchEvent(Event.UpdateLoadingState(loading = false))
320+
BottomBarManager.showAlert(
321+
title = resources.getString(R.string.error_title_staleRates),
322+
message = resources.getString(R.string.error_description_staleRates),
323+
)
324+
return@onEach
325+
}
313326

314327
val bill = Bill.Cash(
315328
token = stateFlow.value.token!!.token,

apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import com.getcode.opencode.exchange.VerifiedFiat
4141
import com.getcode.opencode.exchange.VerifiedFiatCalculator
4242
import com.getcode.opencode.internal.solana.model.SwapId
4343
import com.getcode.opencode.model.core.errors.CheckTokenAvailabilityError
44+
import com.getcode.opencode.model.core.errors.ComputeVerifiedFiatError
4445
import com.getcode.opencode.model.core.errors.GetMintsError
4546
import com.getcode.opencode.model.core.errors.LaunchTokenError
4647
import com.getcode.opencode.model.core.errors.ValidationException
@@ -522,7 +523,14 @@ internal class CurrencyCreatorViewModel @Inject constructor(
522523
amount = event.context.amount,
523524
token = Token.usdf,
524525
rate = Rate.oneToOne,
525-
)
526+
).getOrElse {
527+
BottomBarManager.showAlert(
528+
title = resources.getString(R.string.error_title_staleRates),
529+
message = resources.getString(R.string.error_description_staleRates),
530+
)
531+
return@onEach
532+
}
533+
526534
val feeAmount = event.context.feeAmount?.let { LocalFiat.fromUsd(usdf = it) }
527535
externalWalletController.setAmount(amount = totalAmount, feeAmount = feeAmount)
528536
externalWalletController.setTokenToPurchase(event.context.token)
@@ -537,31 +545,39 @@ internal class CurrencyCreatorViewModel @Inject constructor(
537545
}
538546
.onEach { dispatchEvent(Event.UpdateProcessingState(loading = true)) }
539547
.map { (owner, context) ->
540-
val totalAmount = verifiedFiatCalculator.compute(
548+
verifiedFiatCalculator.compute(
541549
amount = context.amount,
542550
token = Token.usdf,
543551
rate = Rate.oneToOne,
544-
)
545-
val feeAmount = context.feeAmount?.let { LocalFiat.fromUsd(usdf = it) }
546-
transactionController.buy(
547-
owner = owner,
548-
amount = totalAmount,
549-
feeAmount = feeAmount,
550-
of = context.token,
551-
source = SwapFundingSource.SubmitIntent(),
552-
fund = null,
553-
).map { swapId -> swapId to context.token.address }
552+
).mapCatching { totalAmount ->
553+
val feeAmount = context.feeAmount?.let { LocalFiat.fromUsd(usdf = it) }
554+
transactionController.buy(
555+
owner = owner,
556+
amount = totalAmount,
557+
feeAmount = feeAmount,
558+
of = context.token,
559+
source = SwapFundingSource.SubmitIntent(),
560+
fund = null,
561+
).getOrThrow()
562+
}.map { swapId -> swapId to context.token.address }
554563
}
555564
.onResult(
556565
onSuccess = { (swapId, mint) ->
557566
dispatchEvent(Event.PurchaseSubmitted(swapId, mint))
558567
},
559-
onError = {
568+
onError = { cause ->
560569
dispatchEvent(Event.UpdateProcessingState())
561-
BottomBarManager.showError(
562-
title = resources.getString(R.string.error_title_buyNewCurrencyFailed),
563-
message = resources.getString(R.string.error_description_buyNewCurrencyFailed),
564-
)
570+
if (cause is ComputeVerifiedFiatError) {
571+
BottomBarManager.showAlert(
572+
title = resources.getString(R.string.error_title_staleRates),
573+
message = resources.getString(R.string.error_description_staleRates),
574+
)
575+
} else {
576+
BottomBarManager.showError(
577+
title = resources.getString(R.string.error_title_buyNewCurrencyFailed),
578+
message = resources.getString(R.string.error_description_buyNewCurrencyFailed),
579+
)
580+
}
565581
}
566582
)
567583
.launchIn(viewModelScope)

apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/OnRampViewModel.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.getcode.opencode.controllers.TransactionOperations
1818
import com.getcode.opencode.exchange.Exchange
1919
import com.getcode.opencode.exchange.VerifiedFiat
2020
import com.getcode.opencode.exchange.VerifiedFiatCalculator
21+
import com.getcode.opencode.model.core.errors.ComputeVerifiedFiatError
2122
import com.getcode.opencode.model.financial.Currency
2223
import com.getcode.opencode.model.financial.CurrencyCode
2324
import com.getcode.opencode.model.financial.Fiat
@@ -287,7 +288,14 @@ internal class OnRampViewModel @Inject constructor(
287288
amount = localizedAmount,
288289
token = Token.usdf,
289290
rate = rate,
290-
)
291+
).getOrElse { cause ->
292+
dispatchEvent(Event.UpdateConfirmingAmountState())
293+
BottomBarManager.showAlert(
294+
title = resources.getString(R.string.error_title_staleRates),
295+
message = resources.getString(R.string.error_description_staleRates),
296+
)
297+
return@onEach
298+
}
291299

292300
dispatchEvent(Event.OnAmountAccepted(amountFiat))
293301
}.launchIn(viewModelScope)

apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalViewModel.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.getcode.util.resources.ResourceHelper
3434
import com.getcode.utils.base58
3535
import com.getcode.vendor.Base58
3636
import com.flipcash.libs.coroutines.DispatcherProvider
37+
import com.getcode.opencode.model.core.errors.ComputeVerifiedFiatError
3738
import com.getcode.view.BaseViewModel2
3839
import com.getcode.view.LoadingSuccessState
3940
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -275,7 +276,14 @@ internal class WithdrawalViewModel @Inject constructor(
275276
token = token,
276277
balance = stateFlow.value.token!!.balance,
277278
rate = rate,
278-
)
279+
).getOrElse { cause ->
280+
dispatchEvent(Event.UpdateConfirmingAmountState(loading = false))
281+
BottomBarManager.showAlert(
282+
title = resources.getString(R.string.error_title_staleRates),
283+
message = resources.getString(R.string.error_description_staleRates),
284+
)
285+
return@onEach
286+
}
279287

280288
dispatchEvent(Event.UpdateConfirmingAmountState(loading = false, success = true))
281289
dispatchEvent(Event.OnAmountAccepted(amountVerified))
@@ -402,7 +410,14 @@ internal class WithdrawalViewModel @Inject constructor(
402410
token = token,
403411
balance = stateFlow.value.token!!.balance,
404412
rate = exchange.rateToUsd(CurrencyCode.USD)!!,
405-
).localFiat.underlyingTokenAmount
413+
).getOrElse {
414+
dispatchEvent(Event.UpdateWithdrawalState(loading = false))
415+
BottomBarManager.showAlert(
416+
title = resources.getString(R.string.error_title_staleRates),
417+
message = resources.getString(R.string.error_description_staleRates),
418+
)
419+
return@mapNotNull null
420+
}.localFiat.underlyingTokenAmount
406421
}
407422

408423
transactionController.withdraw(

apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenCoordinator.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -477,15 +477,13 @@ class TokenCoordinator @Inject constructor(
477477
updatedTokens = updatedTokens + (mint to updatedToken)
478478

479479
val balance = state.balances[mint] ?: continue
480-
val exchangedValue = runCatching {
481-
verifiedFiatCalculator.compute(
482-
amount = balance,
483-
token = updatedToken,
484-
balance = balance,
485-
rate = Rate.oneToOne,
486-
trace = false,
487-
).localFiat.underlyingTokenAmount
488-
}.getOrNull()
480+
val exchangedValue = verifiedFiatCalculator.compute(
481+
amount = balance,
482+
token = updatedToken,
483+
balance = balance,
484+
rate = Rate.oneToOne,
485+
trace = false,
486+
).getOrNull()?.localFiat?.underlyingTokenAmount
489487

490488
if (exchangedValue != null) {
491489
val newBalance = Fiat.tokenBalance(

apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.getcode.opencode.exchange.Exchange
1818
import com.getcode.opencode.exchange.VerifiedFiat
1919
import com.getcode.opencode.exchange.VerifiedFiatCalculator
2020
import com.getcode.opencode.internal.solana.model.SwapId
21+
import com.getcode.opencode.model.core.errors.ComputeVerifiedFiatError
2122
import com.getcode.opencode.model.core.errors.SwapError
2223
import com.getcode.opencode.model.financial.Currency
2324
import com.getcode.opencode.model.financial.CurrencyCode
@@ -483,7 +484,13 @@ class SwapViewModel @Inject constructor(
483484
token = Token.usdf,
484485
balance = stateFlow.value.reservesBalance.convertingToUsdIfNeeded(rate),
485486
rate = rate
486-
)
487+
).getOrElse {
488+
BottomBarManager.showAlert(
489+
title = resources.getString(R.string.error_title_staleRates),
490+
message = resources.getString(R.string.error_description_staleRates),
491+
)
492+
return@onEach
493+
}
487494
val netAmount = amountFiat.localFiat.nativeAmount
488495

489496
dispatchEvent(Event.UpdateBuyState(loading = true))
@@ -499,7 +506,13 @@ class SwapViewModel @Inject constructor(
499506
amount = Fiat(data.amountData.amount, rate.currency),
500507
token = Token.usdf,
501508
rate = rate,
502-
)
509+
).getOrElse {
510+
BottomBarManager.showAlert(
511+
title = resources.getString(R.string.error_title_staleRates),
512+
message = resources.getString(R.string.error_description_staleRates),
513+
)
514+
return@onEach
515+
}
503516

504517
dispatchEvent(Event.OnAmountAccepted(amountFiat, netTransferAmount = amountFiat.localFiat.nativeAmount))
505518
dispatchEvent(Event.UpdateBuyState(loading = true))
@@ -519,7 +532,13 @@ class SwapViewModel @Inject constructor(
519532
token = tokenWithBalance.token,
520533
balance = tokenWithBalance.balance,
521534
rate = rate,
522-
)
535+
).getOrElse {
536+
BottomBarManager.showAlert(
537+
title = resources.getString(R.string.error_title_staleRates),
538+
message = resources.getString(R.string.error_description_staleRates),
539+
)
540+
return@onEach
541+
}
523542
val netAmount = stateFlow.value.netTransferAmount
524543

525544
dispatchEvent(Event.OnAmountAccepted(amountFiat, netTransferAmount = netAmount))

services/opencode/src/main/kotlin/com/getcode/opencode/exchange/VerifiedFiatCalculator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ interface VerifiedFiatCalculator {
1818
balance: Fiat? = null,
1919
rate: Rate,
2020
trace: Boolean = true,
21-
): VerifiedFiat
21+
): Result<VerifiedFiat>
2222
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/exchange/RealVerifiedFiatCalculator.kt

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.getcode.opencode.model.financial.Fiat
1313
import com.getcode.opencode.model.financial.Fiat.FormattingRule
1414
import com.getcode.opencode.model.financial.LocalFiat
1515
import com.getcode.opencode.model.financial.Rate
16+
import com.getcode.opencode.model.core.errors.ComputeVerifiedFiatError
1617
import com.getcode.opencode.model.financial.Token
1718
import com.getcode.opencode.model.financial.min
1819
import com.getcode.services.opencode.BuildConfig
@@ -68,7 +69,7 @@ internal class RealVerifiedFiatCalculator @Inject constructor(
6869
balance: Fiat?,
6970
rate: Rate,
7071
trace: Boolean,
71-
): VerifiedFiat {
72+
): Result<VerifiedFiat> {
7273
val usdValue = amount.convertingToUsdIfNeeded(rate)
7374
// cap the entered amount as well, since our display rounds HALF_UP
7475
// e,g entered 0.02 USD, but balance is 0.016 USD
@@ -87,10 +88,14 @@ internal class RealVerifiedFiatCalculator @Inject constructor(
8788
} else {
8889
LocalFiat.fromUsd(usdf = cappedValue)
8990
}
90-
return VerifiedFiat(localFiat, verifiedState)
91+
return Result.success(VerifiedFiat(localFiat, verifiedState))
9192
}
9293

93-
val verifiedSupply = verifiedState?.reserveProto?.reserveState?.supplyFromBonding
94+
if (verifiedState == null) {
95+
return Result.failure(ComputeVerifiedFiatError.StaleRate())
96+
}
97+
98+
val verifiedSupply = verifiedState.reserveProto?.reserveState?.supplyFromBonding
9499

95100
val supply = verifiedSupply ?: token.launchpadMetadata?.currentCirculatingSupplyQuarks ?: 0
96101

@@ -99,7 +104,7 @@ internal class RealVerifiedFiatCalculator @Inject constructor(
99104
valueInQuarks = cappedValue.quarks,
100105
currentSupplyInQuarks = supply,
101106
mintDecimals = 6, // usdf is 6 decimals
102-
).getOrThrow()
107+
).getOrElse { return Result.failure(ComputeVerifiedFiatError.ComputationFailed(it)) }
103108

104109
val (quarks, _) = valuation
105110
val units = quarks.units()
@@ -124,15 +129,17 @@ internal class RealVerifiedFiatCalculator @Inject constructor(
124129
)
125130
}
126131

127-
return VerifiedFiat(
128-
localFiat = LocalFiat(
129-
underlyingTokenAmount = underlyingTokenAmount,
130-
// our native amount for the transfer is the valuation of the quarks from a sell
131-
nativeAmount = sellEstimate,
132-
mint = token.address,
133-
rate = Rate(fx = fx, currency = rate.currency),
134-
),
135-
verifiedState = verifiedState,
132+
return Result.success(
133+
VerifiedFiat(
134+
localFiat = LocalFiat(
135+
underlyingTokenAmount = underlyingTokenAmount,
136+
// our native amount for the transfer is the valuation of the quarks from a sell
137+
nativeAmount = sellEstimate,
138+
mint = token.address,
139+
rate = Rate(fx = fx, currency = rate.currency),
140+
),
141+
verifiedState = verifiedState,
142+
)
136143
)
137144
}
138145

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,12 @@ sealed class DiscoverTokensError(
324324
class Unrecognized: DiscoverTokensError("Unrecognized"), NotifiableError
325325
data class Other(override val cause: Throwable? = null) : DiscoverTokensError(message = cause?.message, cause = cause), NotifiableError
326326
}
327+
328+
sealed class ComputeVerifiedFiatError(
329+
override val message: String? = null,
330+
override val cause: Throwable? = null,
331+
) : CodeServerError(message, cause) {
332+
class StaleRate : ComputeVerifiedFiatError("Reserve state unavailable or stale")
333+
data class ComputationFailed(override val cause: Throwable? = null) :
334+
ComputeVerifiedFiatError(message = cause?.message, cause = cause)
335+
}

0 commit comments

Comments
 (0)