Skip to content

Commit ee18157

Browse files
committed
feat: support buys without owning any of token
this unblocks onboarding via a share link and instantly buying via Phantom onramp Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 379a94a commit ee18157

9 files changed

Lines changed: 234 additions & 79 deletions

File tree

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private fun TokenInfoScreen(
8484
.fillMaxWidth()
8585
.padding(horizontal = CodeTheme.dimens.inset),
8686
balance = state.balance.nativeAmount,
87-
appreciation = state.appreciation?.nativeAmount,
87+
appreciation = state.appreciation?.nativeAmount?.takeIf { state.showAppreciation },
8888
onClick = {
8989
dispatch(
9090
TokenInfoViewModel.Event.OpenScreen(
@@ -95,7 +95,7 @@ private fun TokenInfoScreen(
9595
)
9696
}
9797

98-
if (!state.isCashReserve && state.cashReservesEnabled) {
98+
if (!state.isCashReserve && state.showTransactionHistory) {
9999
item {
100100
CodeButton(
101101
modifier = Modifier

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,10 @@ class ExternalWalletDeeplinkState(
378378
val amountToSend = requireNotNull(amount) { "Amount is null" }
379379
val transactionSignature = requireNotNull(signature) { "Transaction not signed" }
380380

381+
val tokenizedOwner = owner.withTimelockForToken(token)
381382
return withContext(NonCancellable) {
382383
transactionController.buy(
383-
owner = owner,
384+
owner = tokenizedOwner,
384385
amount = amountToSend,
385386
of = token,
386387
swapId = swapId,

apps/flipcash/shared/tokens/src/main/kotlin/BuySellSwapTokenViewModel.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import com.getcode.opencode.model.financial.toFiat
2626
import com.getcode.opencode.model.financial.usdf
2727
import com.getcode.opencode.model.transactions.SwapState
2828
import com.getcode.solana.keys.Mint
29+
import com.getcode.solana.keys.base58
2930
import com.getcode.ui.components.text.AmountAnimatedInputUiModel
3031
import com.getcode.ui.components.text.NumberInputHelper
3132
import com.getcode.util.resources.ResourceHelper
33+
import com.getcode.utils.trace
3234
import com.getcode.view.BaseViewModel2
3335
import com.getcode.view.LoadingSuccessState
3436
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -277,7 +279,23 @@ class BuySellSwapTokenViewModel @Inject constructor(
277279
else -> exchange.observeEntryRate()
278280
},
279281
) { tokens, rate ->
280-
val token = tokens.find { it.token.address == mint } ?: return@combine null
282+
var token = tokens.find { it.token.address == mint }
283+
if (token == null) {
284+
val tokenRef = tokenController.getTokenMetadata(mint).getOrNull()
285+
if (tokenRef != null) {
286+
token = TokenWithBalance(
287+
token = tokenRef.token,
288+
balance = Fiat.Zero,
289+
)
290+
}
291+
}
292+
293+
if (token == null) {
294+
trace(tag = "BuySellSwap", message = "Unable to find token for mint ${mint.base58()}")
295+
dispatchEvent(Event.Exit)
296+
return@combine null
297+
}
298+
281299
val balance = LocalFiat(
282300
usdf = token.balance,
283301
nativeAmount = token.balance.convertingTo(rate),

apps/flipcash/shared/tokens/src/main/kotlin/TokenInfoViewModel.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.flipcash.app.tokens.data.Period
3232
import com.flipcash.shared.tokens.R
3333
import com.getcode.manager.BottomBarAction
3434
import com.getcode.manager.BottomBarManager
35+
import com.getcode.opencode.controllers.AccountController
3536
import com.getcode.opencode.controllers.TokenController
3637
import com.getcode.opencode.exchange.Exchange
3738
import com.getcode.opencode.internal.model.WindowedRange
@@ -60,6 +61,7 @@ import kotlin.collections.map
6061

6162
@HiltViewModel
6263
class TokenInfoViewModel @Inject constructor(
64+
private val accountController: AccountController,
6365
private val tokenController: TokenController,
6466
private val exchange: Exchange,
6567
private val shareController: ShareSheetController,
@@ -76,6 +78,8 @@ class TokenInfoViewModel @Inject constructor(
7678
val cashReservesEnabled: Boolean = false,
7779
val marketCapChartEnabled: Boolean = false,
7880
val balance: LocalFiat = LocalFiat.Zero,
81+
val showAppreciation: Boolean = false,
82+
val showTransactionHistory: Boolean = false,
7983
val appreciation: LocalFiat? = null,
8084
val descriptionExpanded: Boolean = false,
8185
val reservesBalance: LocalFiat = LocalFiat.Zero,
@@ -108,6 +112,8 @@ class TokenInfoViewModel @Inject constructor(
108112
data class OnMarketCapPeriodSelected(val period: Period) : Event
109113
data class OnBalanceUpdated(val balance: LocalFiat) : Event
110114
data class OnReservesUpdated(val balance: LocalFiat) : Event
115+
data class OnAppreciatedEnabled(val enabled: Boolean) : Event
116+
data class OnTransactionHistoryEnabled(val enabled: Boolean): Event
111117
data class OnAppreciationUpdated(val amount: LocalFiat?) : Event
112118
data class ExpandDescription(val expand: Boolean) : Event
113119
data object Share : Event
@@ -159,6 +165,7 @@ class TokenInfoViewModel @Inject constructor(
159165
val localizedBalance = LocalFiat(
160166
usdf = balance,
161167
nativeAmount = balance.convertingTo(rate),
168+
mint = token.address,
162169
)
163170

164171
// USD reserves don't appreciate so we track that as MIN_VALUE internally to avoid confusion
@@ -167,6 +174,7 @@ class TokenInfoViewModel @Inject constructor(
167174
LocalFiat(
168175
usdf = appreciation,
169176
nativeAmount = appreciation.convertingTo(rate),
177+
mint = token.address,
170178
)
171179
} else {
172180
null
@@ -236,6 +244,15 @@ class TokenInfoViewModel @Inject constructor(
236244
}
237245
}.launchIn(viewModelScope)
238246

247+
eventFlow
248+
.filterIsInstance<Event.OnBalanceUpdated>()
249+
.mapNotNull { stateFlow.value.mint }
250+
.onEach {
251+
val hasAccount = accountController.hasAccountFor(it)
252+
dispatchEvent(Event.OnAppreciatedEnabled(hasAccount))
253+
dispatchEvent(Event.OnTransactionHistoryEnabled(hasAccount))
254+
}.launchIn(viewModelScope)
255+
239256
eventFlow
240257
.filterIsInstance<Event.OnBalanceUpdated>()
241258
.map { _ ->
@@ -380,6 +397,9 @@ class TokenInfoViewModel @Inject constructor(
380397
state.copy(historicalMarketCapData = historicalData.toMap())
381398
}
382399

400+
is Event.OnAppreciatedEnabled -> { state -> state.copy(showAppreciation = event.enabled) }
401+
is Event.OnTransactionHistoryEnabled -> { state -> state.copy(showTransactionHistory = event.enabled) }
402+
383403
is Event.OnMarketCapPeriodSelected -> { state -> state.copy(selectedPeriod = event.period) }
384404
is Event.OpenScreen -> { state -> state }
385405
is Event.ConnectPhantomWallet -> { state -> state }

libs/currency-math/src/main/kotlin/com/flipcash/libs/currency/math/Estimator.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ object Estimator {
280280
require(feeBps >= 0) { "Fee basis points must be non-negative" }
281281

282282
if (amountInQuarks == 0L) {
283-
println("Sell amount is 0")
284283
return@runCatching SellEstimation(
285284
netAmountToReceive = BigDecimal.ZERO,
286285
fees = BigDecimal.ZERO,

services/opencode/src/main/kotlin/com/getcode/opencode/controllers/CurrencyController.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import com.getcode.solana.keys.Mint
1010
import kotlinx.coroutines.CoroutineScope
1111
import kotlinx.coroutines.channels.awaitClose
1212
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.flow.SharingStarted
1314
import kotlinx.coroutines.flow.callbackFlow
1415
import kotlinx.coroutines.flow.emptyFlow
1516
import kotlinx.coroutines.flow.flatMapLatest
17+
import kotlinx.coroutines.flow.shareIn
1618
import kotlinx.coroutines.launch
1719
import javax.inject.Inject
1820
import javax.inject.Singleton
@@ -39,7 +41,11 @@ class CurrencyController @Inject constructor(
3941
awaitClose {
4042
reference.cancel()
4143
}
42-
}
44+
}.shareIn(
45+
scope = scope,
46+
started = SharingStarted.Lazily,
47+
replay = 1
48+
)
4349
}
4450
}
4551
}

services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TokenController.kt

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
3939
import kotlinx.coroutines.Dispatchers
4040
import kotlinx.coroutines.Job
4141
import kotlinx.coroutines.SupervisorJob
42+
import kotlinx.coroutines.delay
4243
import kotlinx.coroutines.flow.Flow
4344
import kotlinx.coroutines.flow.MutableStateFlow
4445
import kotlinx.coroutines.flow.debounce
@@ -583,6 +584,7 @@ class TokenController @Inject constructor(
583584

584585
private fun streamReserveStates() {
585586
streamReserveStateJob = scope.launch {
587+
delay(100) // Small debounce
586588
trace(
587589
tag = TAG,
588590
message = "Reserve state stream started",
@@ -619,23 +621,30 @@ class TokenController @Inject constructor(
619621
updatedTokens = updatedTokens + (mint to updatedToken)
620622

621623
state.balances[mint]?.let { balance ->
622-
val exchangedValue = LocalFiat.valueExchangeIn(
623-
amount = balance,
624-
token = token,
625-
balance = balance,
626-
rate = Rate.oneToOne,
627-
debug = false,
628-
trace = false,
629-
).underlyingTokenAmount
630-
631-
val newBalance = Fiat.tokenBalance(quarks = exchangedValue.quarks, token = token)
632-
updatedBalances = updatedBalances + (mint to newBalance)
633-
634-
trace(
635-
tag = TAG,
636-
message = "Reserve state updated for ${token.symbol}: supply=${update.reserveState.currentSupply}, balance=${newBalance.formatted()}",
637-
type = TraceType.Process
638-
)
624+
val exchangedValue = runCatching {
625+
LocalFiat.valueExchangeIn(
626+
amount = balance,
627+
token = token,
628+
balance = balance,
629+
rate = Rate.oneToOne,
630+
debug = false,
631+
trace = false,
632+
).underlyingTokenAmount
633+
}.getOrNull()
634+
635+
if (exchangedValue != null) {
636+
val newBalance = Fiat.tokenBalance(
637+
quarks = exchangedValue.quarks,
638+
token = token
639+
)
640+
updatedBalances = updatedBalances + (mint to newBalance)
641+
642+
trace(
643+
tag = TAG,
644+
message = "Reserve state updated for ${token.symbol}: supply=${update.reserveState.currentSupply}, balance=${newBalance.formatted()}",
645+
type = TraceType.Process
646+
)
647+
}
639648
}
640649
}
641650

0 commit comments

Comments
 (0)