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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package com.flipcash.app.deposit

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
Expand All @@ -22,7 +26,6 @@ import com.getcode.navigation.flow.FlowHost
import com.getcode.navigation.flow.LocalFlowNavigator
import com.getcode.navigation.flow.PreviewFlowNavigator
import com.getcode.navigation.flow.deliverFlowResult
import com.getcode.navigation.flow.rememberInitialStack
import com.getcode.navigation.results.NavResultOrCanceled
import com.getcode.navigation.results.NavResultStateRegistry
import com.getcode.solana.keys.Mint
Expand All @@ -35,39 +38,47 @@ fun DepositFlowScreen(
val outerNavigator = LocalCodeNavigator.current
val featureFlags = LocalFeatureFlags.current

val initialStack = route.rememberInitialStack<DepositStep> { steps ->
val directDeposit = featureFlags.observe(FeatureFlag.DepositUsdc).value
val directDeposit by featureFlags
.observe(FeatureFlag.DepositUsdc)
.collectAsStateWithLifecycle()

val initialStack = remember(route, directDeposit) {
@Suppress("UNCHECKED_CAST")
val steps = route.initialStack as List<DepositStep>
println("direct deposit = $directDeposit, isUsdf=${route.mint == Mint.usdf}")
if (!directDeposit && route.mint == Mint.usdf) {
listOf(DepositStep.Destination(route.mint))
} else {
steps
}
}

FlowHost(
initialStack = initialStack,
resultStateRegistry = resultStateRegistry,
onExit = { reason ->
val result: DepositResult = when (reason) {
is FlowExitReason.Completed -> reason.result
FlowExitReason.Canceled,
FlowExitReason.BackedOutOfRoot -> DepositResult.Canceled
}
outerNavigator.deliverFlowResult(
route = route,
value = NavResultOrCanceled.ReturnValue(result),
)
when (result) {
DepositResult.Success -> {
outerNavigator.popUntil { it == AppRoute.Sheets.Menu }
key(directDeposit) {
FlowHost(
initialStack = initialStack,
resultStateRegistry = resultStateRegistry,
onExit = { reason ->
val result: DepositResult = when (reason) {
is FlowExitReason.Completed -> reason.result
FlowExitReason.Canceled,
FlowExitReason.BackedOutOfRoot -> DepositResult.Canceled
}
DepositResult.Canceled -> {
outerNavigator.pop()
outerNavigator.deliverFlowResult(
route = route,
value = NavResultOrCanceled.ReturnValue(result),
)
when (result) {
DepositResult.Success -> {
outerNavigator.popUntil { it == AppRoute.Sheets.Menu }
}
DepositResult.Canceled -> {
outerNavigator.pop()
}
}
}
},
entryProvider = depositEntryProvider(route.mint),
)
},
entryProvider = depositEntryProvider(route.mint),
)
}
}

private fun depositEntryProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ import com.getcode.solana.keys.Mint
import com.getcode.solana.keys.base58
import com.getcode.util.resources.ResourceHelper
import com.flipcash.libs.coroutines.DispatcherProvider
import com.getcode.opencode.internal.solana.extensions.timelockSwapAccounts
import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.featureflags.FeatureFlagController
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.financial.usdf
import com.getcode.view.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
Expand Down Expand Up @@ -65,10 +63,7 @@ internal class DepositViewModel @Inject constructor(
viewModelScope.launch {
val directDeposit = featureFlags.get(FeatureFlag.DepositUsdc)
val address = if (result.token.address == Mint.usdf && directDeposit) {
val usdfSwapAccounts = userManager.accountCluster?.let {
Token.usdf.timelockSwapAccounts(it.authorityPublicKey)
}
usdfSwapAccounts?.ata?.publicKey?.base58()
userManager.accountCluster?.authorityPublicKey?.base58()
} else {
userManager.accountCluster?.depositAddressFor(result.token)?.base58()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ sealed interface FeatureFlag<T: Any> {
@FeatureFlagMarker
data object DepositUsdc: FeatureFlag<Boolean> {
override val key: String = "deposit_usdc_enabled"
override val default: Boolean = false
override val launched: Boolean = false
override val default: Boolean = true
override val launched: Boolean = true
override val visible: Boolean = true
override val persistLogOut: Boolean = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.flipcash.app.shareable.Shareable
import com.flipcash.app.shareable.ShareableConfirmationController
import com.flipcash.app.tokens.TokenCoordinator
import com.flipcash.app.tokens.TokenUpdater
import com.flipcash.app.tokens.UsdcDepositSweep
import com.flipcash.core.R
import com.flipcash.services.controllers.AccountController
import com.flipcash.services.controllers.SettingsController
Expand Down Expand Up @@ -122,6 +123,7 @@ class RealSessionController @Inject constructor(
private val tokenCoordinator: TokenCoordinator,
private val featureFlagController: FeatureFlagController,
private val analytics: FlipcashAnalyticsService,
private val usdcSweep: UsdcDepositSweep,
appSettingsCoordinator: AppSettingsCoordinator,
) : SessionController {

Expand All @@ -147,6 +149,7 @@ class RealSessionController @Inject constructor(
when {
authState is AuthState.LoggedOut -> {
stopPolling()
cancelUpdates()
_state.update { SessionState() }
}
authState.isAtLeastRegistered -> {
Expand Down Expand Up @@ -196,6 +199,7 @@ class RealSessionController @Inject constructor(
.onEach {
if (userManager.authState.isAtLeastRegistered) {
updateUserFlags()
swapUsdcIfNeeded()
}
}.launchIn(scope)
}
Expand All @@ -218,6 +222,7 @@ class RealSessionController @Inject constructor(
type = TraceType.Process,
)
startPolling()
swapUsdcIfNeeded()
updateUserFlags()
updateSettings()
checkPendingItemsInFeed()
Expand All @@ -240,6 +245,7 @@ class RealSessionController @Inject constructor(
*/
override fun onAppInBackground() {
stopPolling()
cancelUpdates()
billingClient.disconnect()

toastController.clear()
Expand All @@ -260,12 +266,23 @@ class RealSessionController @Inject constructor(
}
}

private fun swapUsdcIfNeeded() {
val owner = userManager.accountCluster ?: return
if (userManager.authState.canAccessAuthenticatedApis) {
usdcSweep.execute(owner)
}
}

private fun stopPolling() {
tokenUpdater.stop()
activityFeedUpdater.stop()
profileUpdater.stop()
}

private fun cancelUpdates() {
usdcSweep.cancel()
}

private fun updateUserFlags() {
if (userManager.authState.isAtLeastRegistered) {
scope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class SessionControllerGiftCardErrorTest {
featureFlagController = mockk(relaxed = true),
analytics = analytics,
appSettingsCoordinator = mockk(relaxed = true),
usdcSweep = mockk(relaxed = true),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.flipcash.app.tokens

import com.getcode.opencode.controllers.AccountController
import com.getcode.opencode.controllers.TransactionOperations
import com.getcode.opencode.model.accounts.AccountCluster
import com.getcode.opencode.model.accounts.AccountFilter
import com.getcode.opencode.model.accounts.AccountType
import com.getcode.solana.keys.Mint
import com.getcode.solana.keys.base58
import com.getcode.utils.TraceType
import com.getcode.utils.network.retryable
import com.getcode.utils.trace
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

class UsdcDepositSweep(
private val transactionOperations: TransactionOperations,
private val accountController: AccountController,
private val tokenCoordinator: TokenCoordinator,
private val maxRetries: Int = MAX_RETRIES,
private val initialDelay: Duration = INITIAL_DELAY,
private val backoffFactor: Double = BACKOFF_FACTOR,
) {
@Inject constructor(
transactionOperations: TransactionOperations,
accountController: AccountController,
tokenCoordinator: TokenCoordinator,
) : this(
transactionOperations = transactionOperations,
accountController = accountController,
tokenCoordinator = tokenCoordinator,
maxRetries = MAX_RETRIES,
initialDelay = INITIAL_DELAY,
backoffFactor = BACKOFF_FACTOR
)

private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var activeJob: Job? = null

fun execute(owner: AccountCluster) {
if (activeJob?.isActive == true) return
activeJob = scope.launch {
val amount = retryable(
maxRetries = maxRetries,
delayDuration = initialDelay,
backoffFactor = backoffFactor,
) {
val usdcAccount = accountController.getAccount(
accountOwner = owner,
requestingOwner = owner,
filter = AccountFilter.MintAddress(Mint.usdc),
).getOrNull()?.takeIf { account ->
account.accountType == AccountType.AssociatedToken
}

usdcAccount?.let {
trace(tag = TAG, message = "USDC ATA found. => ${it.address.base58()}")
} ?: trace(tag = TAG, message = "USDC ATA not found")

val balance = usdcAccount?.balance ?: 0L
check(balance > 0L) { "USDC balance <= 0. nothing to sweep" }
balance
} ?: return@launch

trace(tag = TAG, message = "Swapping $amount USDC to USDF", type = TraceType.Process)

transactionOperations.swapUsdc(
owner = owner,
amount = amount,
).onSuccess {
trace(tag = TAG, message = "USDC→USDF sweep completed")
tokenCoordinator.update()
}.onFailure { error ->
trace(tag = TAG, message = "USDC→USDF sweep failed: ${error.message}", error = error)
}
}
}

fun cancel() {
activeJob?.cancel()
activeJob = null
}

companion object {
private const val TAG = "UsdcDepositSweep"
private const val MAX_RETRIES = 5
private val INITIAL_DELAY = 5.seconds
private const val BACKOFF_FACTOR = 2.0
}
}
Loading
Loading