Skip to content

Commit 7823a86

Browse files
committed
feat(opencode): pin VerifiedState from compute() through to intent submission
Thread VerifiedFiat (LocalFiat + VerifiedState) atomically from VerifiedFiatCalculator.compute() through ViewModels to TransactionController, eliminating TOCTOU race where bonding curve supply could diverge between amount computation and intent submission. - Add VerifiedFiat wrapper type pairing LocalFiat with pinned VerifiedState - Update TransactionOperations buy/sell/withdraw to accept VerifiedFiat - Thread VerifiedFiat through SwapViewModel, WithdrawalViewModel, OnRampViewModel, CashScreenViewModel, CurrencyCreatorViewModel - Thread through ExternalWalletOnRampState and CoinbaseOnRampState pipelines - Add LocalFiat.rounded() and fix totalBalance penny rounding discrepancy Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 2ede53f commit 7823a86

22 files changed

Lines changed: 157 additions & 112 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ internal class CashScreenViewModel @Inject constructor(
146146
amount = Fiat(amount, rate.currency),
147147
token = token,
148148
rate = rate,
149-
)
149+
).localFiat
150150

151151
val neededAmount = amountFiat.nativeAmount - tokenBalance
152152
println("entered amount ${amountFiat.nativeAmount}, tokenbalace=$tokenBalance, needed=$neededAmount")
@@ -304,7 +304,7 @@ internal class CashScreenViewModel @Inject constructor(
304304
val (token, balance) = stateFlow.value.token!!
305305
val rate = exchange.entryRate
306306

307-
val amountFiat = verifiedFiatCalculator.compute(
307+
val result = verifiedFiatCalculator.compute(
308308
amount = Fiat(data.amountData.amount, rate.currency),
309309
token = token,
310310
balance = balance.underlyingTokenAmount,
@@ -313,7 +313,8 @@ internal class CashScreenViewModel @Inject constructor(
313313

314314
val bill = Bill.Cash(
315315
token = stateFlow.value.token!!.token,
316-
amount = amountFiat
316+
amount = result.localFiat,
317+
verifiedState = result.verifiedState,
317318
)
318319

319320
dispatchEvent(Event.UpdateLoadingState(loading = false, success = true))

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,22 @@ import com.flipcash.services.user.UserManager
3737
import com.getcode.manager.BottomBarManager
3838
import com.getcode.opencode.controllers.CurrencyController
3939
import com.getcode.opencode.controllers.TransactionController
40+
import com.getcode.opencode.exchange.VerifiedFiat
41+
import com.getcode.opencode.exchange.VerifiedFiatCalculator
4042
import com.getcode.opencode.internal.solana.model.SwapId
4143
import com.getcode.opencode.model.core.errors.CheckTokenAvailabilityError
4244
import com.getcode.opencode.model.core.errors.GetMintsError
4345
import com.getcode.opencode.model.core.errors.LaunchTokenError
4446
import com.getcode.opencode.model.core.errors.ValidationException
4547
import com.getcode.opencode.model.financial.MintMetadata
48+
import com.getcode.opencode.model.financial.Rate
4649
import com.getcode.opencode.model.financial.Token
4750
import com.getcode.opencode.model.financial.TokenCreateRequest
4851
import com.getcode.opencode.model.financial.fromLaunch
4952
import com.getcode.opencode.model.financial.minus
5053
import com.getcode.opencode.model.financial.orZero
5154
import com.getcode.opencode.model.financial.plus
55+
import com.getcode.opencode.model.financial.usdf
5256
import com.getcode.opencode.model.moderation.ModerationAttestation
5357
import com.getcode.opencode.model.transactions.SwapFundingSource
5458
import com.getcode.solana.keys.Mint
@@ -87,6 +91,7 @@ internal class CurrencyCreatorViewModel @Inject constructor(
8791
moderationController: ModerationController,
8892
currencyController: CurrencyController,
8993
transactionController: TransactionController,
94+
private val verifiedFiatCalculator: VerifiedFiatCalculator,
9095
externalWalletController: ExternalWalletOnRampController,
9196
tokenCoordinator: TokenCoordinator,
9297
balancePoller: BalancePoller,
@@ -513,8 +518,11 @@ internal class CurrencyCreatorViewModel @Inject constructor(
513518
AppRoute.Token.CurrencyCreator,
514519
OnRampProvider.Phantom
515520
)
516-
val totalAmount = LocalFiat(usdf = event.context.amount)
517-
println("total amount ${totalAmount.underlyingTokenAmount}")
521+
val totalAmount = verifiedFiatCalculator.compute(
522+
amount = event.context.amount,
523+
token = Token.usdf,
524+
rate = Rate.oneToOne,
525+
)
518526
val feeAmount = event.context.feeAmount?.let { LocalFiat(usdf = it) }
519527
externalWalletController.setAmount(amount = totalAmount, feeAmount = feeAmount)
520528
externalWalletController.setTokenToPurchase(event.context.token)
@@ -529,10 +537,16 @@ internal class CurrencyCreatorViewModel @Inject constructor(
529537
}
530538
.onEach { dispatchEvent(Event.UpdateProcessingState(loading = true)) }
531539
.map { (owner, context) ->
540+
val totalAmount = verifiedFiatCalculator.compute(
541+
amount = context.amount,
542+
token = Token.usdf,
543+
rate = Rate.oneToOne,
544+
)
545+
val feeAmount = context.feeAmount?.let { LocalFiat(usdf = it) }
532546
transactionController.buy(
533547
owner = owner,
534-
amount = LocalFiat(usdf = context.amount),
535-
feeAmount = context.feeAmount?.let { LocalFiat(usdf = it) },
548+
amount = totalAmount,
549+
feeAmount = feeAmount,
536550
of = context.token,
537551
source = SwapFundingSource.SubmitIntent(),
538552
fund = null,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
99
import androidx.compose.ui.res.stringResource
1010
import com.flipcash.app.core.AppRoute
1111
import com.getcode.solana.keys.Mint
12+
1213
import com.flipcash.app.onramp.internal.OnRampViewModel
1314
import com.flipcash.app.onramp.internal.screens.OnRampAmountScreen
1415
import com.flipcash.features.onramp.R

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import com.getcode.manager.BottomBarManager
1616
import com.getcode.opencode.controllers.TokenController
1717
import com.getcode.opencode.controllers.TransactionOperations
1818
import com.getcode.opencode.exchange.Exchange
19+
import com.getcode.opencode.exchange.VerifiedFiat
20+
import com.getcode.opencode.exchange.VerifiedFiatCalculator
1921
import com.getcode.opencode.model.financial.Currency
2022
import com.getcode.opencode.model.financial.CurrencyCode
2123
import com.getcode.opencode.model.financial.Fiat
@@ -24,6 +26,7 @@ import com.getcode.opencode.model.financial.LocalFiat
2426
import com.getcode.opencode.model.financial.SendLimit
2527
import com.getcode.opencode.model.financial.Token
2628
import com.getcode.opencode.model.financial.toFiat
29+
import com.getcode.opencode.model.financial.usdf
2730
import com.getcode.solana.keys.Mint
2831
import com.getcode.ui.components.text.AmountAnimatedInputUiModel
2932
import com.getcode.ui.components.text.NumberInputHelper
@@ -45,7 +48,7 @@ internal data class AmountEntryState(
4548
val currencyModel: CurrencyHolder = CurrencyHolder(),
4649
val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(),
4750
val confirmingAmount: LoadingSuccessState = LoadingSuccessState(),
48-
val selectedAmount: LocalFiat = LocalFiat.Zero,
51+
val selectedAmount: VerifiedFiat = VerifiedFiat(LocalFiat.Zero, null),
4952
) {
5053
val canAdd: Boolean
5154
get() = (amountAnimatedModel.amountData.amount) > 0.00
@@ -71,6 +74,7 @@ internal data class AmountEntryState(
7174
@HiltViewModel
7275
internal class OnRampViewModel @Inject constructor(
7376
private val exchange: Exchange,
77+
private val verifiedFiatCalculator: VerifiedFiatCalculator,
7478
private val resources: ResourceHelper,
7579
private val onRampController: CoinbaseOnRampController,
7680
tokenController: TokenController,
@@ -127,9 +131,9 @@ internal class OnRampViewModel @Inject constructor(
127131
val success: Boolean = false
128132
) : Event
129133

130-
data class OnAmountAccepted(val amount: LocalFiat) : Event
134+
data class OnAmountAccepted(val amount: VerifiedFiat) : Event
131135

132-
data class CreateAndSendTransactionToWallet(val amount: LocalFiat) : Event
136+
data class CreateAndSendTransactionToWallet(val amount: VerifiedFiat) : Event
133137
// endregion
134138
}
135139

@@ -279,9 +283,10 @@ internal class OnRampViewModel @Inject constructor(
279283
}
280284
}
281285

282-
val amountFiat = LocalFiat(
283-
usdf = localizedAmount.convertingTo(exchange.rateToUsd(rate.currency)!!),
284-
nativeAmount = localizedAmount,
286+
val amountFiat = verifiedFiatCalculator.compute(
287+
amount = localizedAmount,
288+
token = Token.usdf,
289+
rate = rate,
285290
)
286291

287292
dispatchEvent(Event.OnAmountAccepted(amountFiat))
@@ -314,9 +319,9 @@ internal class OnRampViewModel @Inject constructor(
314319
}
315320

316321
onRampController.placeOrderAndStartPayment(
317-
amount = selectedAmount.underlyingTokenAmount,
322+
amount = selectedAmount.localFiat.underlyingTokenAmount,
318323
token = token,
319-
localFiat = selectedAmount,
324+
verifiedFiat = selectedAmount,
320325
).onFailure { error ->
321326
dispatchEvent(Event.UpdateConfirmingAmountState())
322327
when (error) {

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.getcode.manager.BottomBarAction
1717
import com.getcode.manager.BottomBarManager
1818
import com.getcode.opencode.controllers.TransactionOperations
1919
import com.getcode.opencode.exchange.Exchange
20+
import com.getcode.opencode.exchange.VerifiedFiat
2021
import com.getcode.opencode.exchange.VerifiedFiatCalculator
2122
import com.getcode.opencode.model.financial.Currency
2223
import com.getcode.opencode.model.financial.CurrencyCode
@@ -56,7 +57,7 @@ internal data class AmountEntryState(
5657
val currencyModel: CurrencyHolder = CurrencyHolder(),
5758
val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(),
5859
val confirmingAmount: LoadingSuccessState = LoadingSuccessState(),
59-
val selectedAmount: LocalFiat = LocalFiat.Zero,
60+
val selectedAmount: VerifiedFiat = VerifiedFiat(LocalFiat.Zero, null),
6061
)
6162

6263
internal data class DestinationState(
@@ -123,7 +124,7 @@ internal class WithdrawalViewModel @Inject constructor(
123124
data class OnAmountChanged(val amountAnimatedModel: AmountAnimatedInputUiModel) : Event
124125
data class OnCurrencyChanged(val currency: Currency) : Event
125126
data object OnAmountConfirmed : Event
126-
data class OnAmountAccepted(val amount: LocalFiat) : Event
127+
data class OnAmountAccepted(val amount: VerifiedFiat) : Event
127128
data object OnDestinationConfirmed : Event
128129
data class UpdateConfirmingAmountState(
129130
val loading: Boolean = false,
@@ -269,15 +270,15 @@ internal class WithdrawalViewModel @Inject constructor(
269270
dispatchEvent(Event.UpdateConfirmingAmountState(loading = true))
270271
val rate = exchange.rateForUsd()
271272
val token = stateFlow.value.token!!.token
272-
val amountFiat = verifiedFiatCalculator.compute(
273+
val amountVerified = verifiedFiatCalculator.compute(
273274
amount = Fiat(data.amountData.amount, rate.currency),
274275
token = token,
275276
balance = stateFlow.value.token!!.balance,
276277
rate = rate,
277278
)
278279

279280
dispatchEvent(Event.UpdateConfirmingAmountState(loading = false, success = true))
280-
dispatchEvent(Event.OnAmountAccepted(amountFiat))
281+
dispatchEvent(Event.OnAmountAccepted(amountVerified))
281282
}.launchIn(viewModelScope)
282283

283284
eventFlow
@@ -341,7 +342,7 @@ internal class WithdrawalViewModel @Inject constructor(
341342
eventFlow
342343
.filterIsInstance<Event.OnWithdraw>()
343344
.onEach {
344-
val amount = stateFlow.value.amountEntryState.selectedAmount
345+
val amount = stateFlow.value.amountEntryState.selectedAmount.localFiat
345346
val withdrawalChecks = stateFlow.value.destinationState.availability
346347
val fee = withdrawalChecks?.feeAmount
347348
if (amount.nativeAmount - (fee ?: Fiat.Zero) < Fiat.Zero) {
@@ -401,7 +402,7 @@ internal class WithdrawalViewModel @Inject constructor(
401402
token = token,
402403
balance = stateFlow.value.token!!.balance,
403404
rate = exchange.rateToUsd(CurrencyCode.USD)!!,
404-
).underlyingTokenAmount
405+
).localFiat.underlyingTokenAmount
405406
}
406407

407408
transactionController.withdraw(
@@ -417,7 +418,7 @@ internal class WithdrawalViewModel @Inject constructor(
417418
onError = {
418419
analytics.transfer(
419420
event = Analytics.Transfer.Withdrawal,
420-
amount = stateFlow.value.amountEntryState.selectedAmount,
421+
amount = stateFlow.value.amountEntryState.selectedAmount.localFiat,
421422
successful = false,
422423
error = it,
423424
)
@@ -430,7 +431,7 @@ internal class WithdrawalViewModel @Inject constructor(
430431
onSuccess = {
431432
analytics.transfer(
432433
event = Analytics.Transfer.Withdrawal,
433-
amount = stateFlow.value.amountEntryState.selectedAmount,
434+
amount = stateFlow.value.amountEntryState.selectedAmount.localFiat,
434435
)
435436
viewModelScope.launch {
436437
coroutineScope {

apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/confirmation/WithdrawalConfirmationScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private fun WithdrawalConfirmationScreenContent(
7373
TransferInfo(
7474
tokenWithBalance = TokenWithBalance(
7575
state.token!!.token,
76-
balance = state.amountEntryState.selectedAmount.nativeAmount,
76+
balance = state.amountEntryState.selectedAmount.localFiat.nativeAmount,
7777
),
7878
destination = state.destinationState.textFieldState.text.toString(),
7979
fee = state.destinationState.availability?.feeAmount,

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampController.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import com.getcode.network.jwt.Jwt
1616
import com.getcode.network.jwt.JwtSecuredEndpoint
1717
import com.getcode.opencode.controllers.TransactionOperations
1818
import com.getcode.opencode.exchange.Exchange
19+
import com.getcode.opencode.exchange.VerifiedFiat
1920
import com.getcode.opencode.internal.solana.extensions.timelockSwapAccounts
2021
import com.getcode.opencode.internal.solana.model.SwapId
2122
import com.getcode.opencode.model.financial.CurrencyCode
2223
import com.getcode.opencode.model.financial.Fiat
23-
import com.getcode.opencode.model.financial.LocalFiat
24+
2425
import com.getcode.opencode.model.financial.Token
2526
import com.getcode.opencode.model.financial.usdf
2627
import com.getcode.opencode.model.transactions.SwapFundingSource
@@ -58,7 +59,7 @@ class CoinbaseOnRampController @Inject constructor(
5859
private val _state = MutableStateFlow<CoinbaseOnRampState>(CoinbaseOnRampState.Idle)
5960
val state: StateFlow<CoinbaseOnRampState> = _state.asStateFlow()
6061

61-
fun startPayment(order: OnrampOrder, token: Token, amount: LocalFiat) {
62+
fun startPayment(order: OnrampOrder, token: Token, amount: VerifiedFiat) {
6263
_state.value = CoinbaseOnRampState.Paying(order, token, amount)
6364
}
6465

@@ -86,7 +87,7 @@ class CoinbaseOnRampController @Inject constructor(
8687
suspend fun placeOrderAndStartPayment(
8788
amount: Fiat,
8889
token: Token,
89-
localFiat: LocalFiat,
90+
verifiedFiat: VerifiedFiat,
9091
): Result<Unit> {
9192
when (googlePayReadiness.check()) {
9293
GooglePayReadiness.Status.NotSupported ->
@@ -99,7 +100,7 @@ class CoinbaseOnRampController @Inject constructor(
99100
return placeOrderInclusiveOfFees(amount)
100101
.map { (orderId, paymentLink) ->
101102
val order = OnrampOrder(orderId, paymentLink.url)
102-
startPayment(order, token, localFiat)
103+
startPayment(order, token, verifiedFiat)
103104
}
104105
}
105106

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampState.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package com.flipcash.app.onramp
33
import android.os.Parcelable
44
import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError
55
import com.getcode.opencode.internal.solana.model.SwapId
6-
import com.getcode.opencode.model.financial.LocalFiat
6+
import com.getcode.opencode.exchange.VerifiedFiat
7+
78
import com.getcode.opencode.model.financial.Token
89
import kotlinx.parcelize.Parcelize
910

@@ -15,8 +16,8 @@ data class OnrampOrder(
1516

1617
sealed interface CoinbaseOnRampState {
1718
data object Idle : CoinbaseOnRampState
18-
data class Paying(val order: OnrampOrder, val token: Token, val amount: LocalFiat) : CoinbaseOnRampState
19-
data class Processing(val orderId: String, val token: Token, val amount: LocalFiat) : CoinbaseOnRampState
19+
data class Paying(val order: OnrampOrder, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState
20+
data class Processing(val orderId: String, val token: Token, val amount: VerifiedFiat) : CoinbaseOnRampState
2021
data class Completed(val swapId: SwapId) : CoinbaseOnRampState
2122
data class Failed(val error: CoinbaseOnRampWebError) : CoinbaseOnRampState
2223
}

apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampController.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package com.flipcash.app.onramp
22

33
import com.flipcash.app.core.AppRoute
44
import com.flipcash.app.core.encryption.boxOpen
5-
import com.flipcash.app.core.encryption.toPublicKey
65
import com.flipcash.app.core.navigation.DeeplinkType
76
import com.flipcash.app.core.onramp.deeplinks.ExternalWalletConnection
87
import com.flipcash.app.core.onramp.deeplinks.ExternallySignedTransaction
98
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
109
import com.flipcash.services.user.UserManager
1110
import com.getcode.opencode.controllers.TransactionOperations
11+
import com.getcode.opencode.exchange.VerifiedFiat
1212
import com.getcode.opencode.internal.solana.extensions.deriveAssociatedAccount
1313
import com.getcode.opencode.internal.solana.model.LiquidityPool
1414
import com.getcode.opencode.internal.solana.model.SwapId
@@ -68,8 +68,8 @@ class ExternalWalletOnRampController @Inject constructor(
6868
private val _pendingNavigation = MutableSharedFlow<AppRoute>(extraBufferCapacity = 1)
6969
val pendingNavigation: SharedFlow<AppRoute> = _pendingNavigation.asSharedFlow()
7070

71-
private val _amount = MutableStateFlow<LocalFiat?>(null)
72-
val amount: StateFlow<LocalFiat?> = _amount.asStateFlow()
71+
private val _amount = MutableStateFlow<VerifiedFiat?>(null)
72+
val amount: StateFlow<VerifiedFiat?> = _amount.asStateFlow()
7373

7474
private val _feeAmount = MutableStateFlow<LocalFiat?>(null)
7575
val feeAmount: StateFlow<LocalFiat?> = _feeAmount.asStateFlow()
@@ -85,7 +85,7 @@ class ExternalWalletOnRampController @Inject constructor(
8585
_state.value = ExternalWalletOnRampState.Started(origin, provider)
8686
}
8787

88-
fun setAmount(amount: LocalFiat?, feeAmount: LocalFiat? = null) {
88+
fun setAmount(amount: VerifiedFiat?, feeAmount: LocalFiat? = null) {
8989
_amount.value = amount
9090
_feeAmount.value = feeAmount
9191
}
@@ -156,7 +156,7 @@ class ExternalWalletOnRampController @Inject constructor(
156156
val amount = _amount.value ?: return
157157
val fee = _feeAmount.value ?: LocalFiat.Zero
158158

159-
createUsdcToUsdfSwapTransaction(state, amount)
159+
createUsdcToUsdfSwapTransaction(state, amount.localFiat)
160160
.onFailure { fail(DeeplinkOnRampError.FailedToCreateTransaction(message = it.message, cause = it), state) }
161161
.fold(
162162
onSuccess = { (transaction, swapId) ->
@@ -194,7 +194,7 @@ class ExternalWalletOnRampController @Inject constructor(
194194
val amount = _amount.value ?: return
195195
val fee = _feeAmount.value ?: LocalFiat.Zero
196196

197-
createDepositTransaction(state, amount)
197+
createDepositTransaction(state, amount.localFiat)
198198
.onFailure { fail(DeeplinkOnRampError.FailedToCreateTransaction(message = it.message, cause = it), state) }
199199
.fold(
200200
onSuccess = { transaction ->

apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ fun ExternalWalletOnRampHandler(
177177
)
178178
}
179179

180-
analytics.amountSelectedForWalletTransfer(current.provider, current.amount.underlyingTokenAmount)
180+
analytics.amountSelectedForWalletTransfer(current.provider, current.amount.localFiat.underlyingTokenAmount)
181181
uriHandler.openUri(uri.toString())
182182
}
183183

0 commit comments

Comments
 (0)