Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-23 18:01:25 +05:00
parent 53b02cfb10
commit 2be7445212
81 changed files with 4444 additions and 3434 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.domain.express.models
import java.math.BigDecimal
@Suppress("MagicNumber")
sealed class ExpressError : Throwable() {
abstract val code: Int
@ -61,4 +62,12 @@ sealed class ExpressError : Throwable() {
data object UnknownError : ExpressError() {
override val code: Int = -1
}
data class TooLargeSolanaTransactionError(override val code: Int = -2) : ExpressError() {
override val message: String = "tooLargeSolanaTransaction"
}
data class DexActiveSupplyError(override val code: Int = -3) : ExpressError() {
override val message: String = "dexActiveSupplyError"
}
}

View file

@ -23,14 +23,18 @@ enum class ExpressProviderType(val typeName: String) {
ONRAMP(typeName = "ONRAMP"),
;
fun shouldStoreSwapTransaction() = when (this) {
CEX,
DEX_BRIDGE,
DEX,
-> true
ONRAMP,
-> false
}
companion object {
fun ExpressProviderType.shouldStoreSwapTransaction() = when (this) {
CEX,
DEX_BRIDGE,
DEX,
-> true
ONRAMP,
-> false
fun getSwapProviderTypes(): List<ExpressProviderType> {
return listOf(CEX, DEX, DEX_BRIDGE)
}
}
}

View file

@ -31,11 +31,22 @@ interface RampStateManager {
sendUnavailabilityReason: ScenarioUnavailabilityReason?,
): Either<ScenarioUnavailabilityReason, Unit>
/**
* Check if [CryptoCurrency] is available for swap (express/assets request)
*
* @param userWalletId the ID of the user's wallet
* @param cryptoCurrency cryptocurrency
*/
suspend fun availableForSwap(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ScenarioUnavailabilityReason
suspend fun availableForSwap(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Map<CryptoCurrency, ScenarioUnavailabilityReason>
suspend fun fetchSellServiceData()
fun getSellInitializationStatus(): Flow<Lce<Throwable, Any>>

View file

@ -0,0 +1,33 @@
package com.tangem.domain.swap.models
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
/**
* Represents the status of a cryptocurrency in the context of a swap operation.
*
* Combines the [UserWallet], [CryptoCurrencyStatus], and [Account] to provide
* all necessary information about a currency participating in a swap.
*
* @property userWallet the user wallet that owns the currency
* @property status the current status of the cryptocurrency, including balance and value state
* @property account the account within the wallet that holds the currency
* @property currency shortcut to the [CryptoCurrency] from [status]
* @property userWalletId shortcut to the wallet ID from [userWallet]
* @property isAvailableForSwap whether this currency can participate in a swap operation,
* determined by [RampStateManager][com.tangem.domain.exchange.RampStateManager]
*/
data class SwapCurrencyStatus(
val userWallet: UserWallet,
val status: CryptoCurrencyStatus,
val account: Account,
val isAvailableForSwap: Boolean = true,
) {
val currency: CryptoCurrency
get() = status.currency
val userWalletId: UserWalletId
get() = userWallet.walletId
}

View file

@ -9,7 +9,8 @@ import java.math.BigDecimal
* List of saved swap transactions
*/
data class SwapTransactionListModel(
val userWalletId: String,
val fromUserWalletId: String,
val toUserWalletId: String,
val fromCryptoCurrencyId: String,
val toCryptoCurrencyId: String,
val fromCryptoCurrency: CryptoCurrency,

View file

@ -16,6 +16,21 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
interface SwapRepositoryV2 {
/**
* Returns express swap pairs for a specific primary and secondary currency.
*
* @param primarySwapCurrencyStatus primary currency status participating in the swap
* @param secondarySwapCurrencyStatus secondary currency status participating in the swap
* @param filterProviderTypes filters only specified provider types, if empty returns providers as is
* @param swapTxType swap tx type
*/
suspend fun getPairs(
primarySwapCurrencyStatus: SwapCurrencyStatus,
secondarySwapCurrencyStatus: SwapCurrencyStatus,
filterProviderTypes: List<ExpressProviderType>,
swapTxType: SwapTxType,
): List<SwapPairModel>
/**
* Express swap pairs, both direct and reversed
*
@ -84,7 +99,7 @@ interface SwapRepositoryV2 {
userWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
toCryptoCurrency: CryptoCurrency,
amount: String,
amount: BigDecimal,
amountType: SwapAmountType,
toAddress: String,
toExtraId: String?,

View file

@ -18,7 +18,8 @@ interface SwapTransactionRepository {
/**
* Store new swap transaction
*
* @param userWalletId selected user wallet id
* @param fromUserWalletId wallet id swap from
* @param toUserWalletId wallet id swap to
* @param fromCryptoCurrency currency swap from
* @param toCryptoCurrency currency swap to
* @param fromAccount account swap from
@ -27,7 +28,8 @@ interface SwapTransactionRepository {
*/
@Suppress("LongParameterList")
suspend fun storeTransaction(
userWalletId: UserWalletId,
fromUserWalletId: UserWalletId,
toUserWalletId: UserWalletId,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
fromAccount: Account?,

View file

@ -12,6 +12,7 @@ import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapAmountType
import com.tangem.domain.swap.models.SwapDataModel
import java.math.BigDecimal
@Suppress("LongParameterList")
class GetSwapDataUseCase(
@ -22,7 +23,7 @@ class GetSwapDataUseCase(
suspend operator fun invoke(
userWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
amount: String,
amount: BigDecimal,
amountType: SwapAmountType,
toCryptoCurrency: CryptoCurrency,
toAddress: String,

View file

@ -0,0 +1,35 @@
package com.tangem.domain.swap.usecase
import arrow.core.Either
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapCurrencyStatus
import com.tangem.domain.swap.models.SwapTxType
/**
* Use case for retrieving swap pairs between a specific primary and secondary currency.
*
* Returns either a list of available swap pairs or a resolved swap error.
*
* @property swapRepositoryV2 repository providing swap pair data
* @property swapErrorResolver resolver that maps exceptions to domain swap errors
*/
class GetSwapPairUseCase(
private val swapRepositoryV2: SwapRepositoryV2,
private val swapErrorResolver: SwapErrorResolver,
) {
suspend operator fun invoke(
primarySwapCurrencyStatus: SwapCurrencyStatus,
secondarySwapCurrencyStatus: SwapCurrencyStatus,
filterProviderTypes: List<ExpressProviderType>,
swapTxType: SwapTxType,
) = Either.catch {
swapRepositoryV2.getPairs(
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
filterProviderTypes = filterProviderTypes,
swapTxType = swapTxType,
)
}.mapLeft(swapErrorResolver::resolve)
}

View file

@ -2,7 +2,6 @@ package com.tangem.domain.swap.usecase
import arrow.core.Either
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType.Companion.shouldStoreSwapTransaction
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -19,7 +18,8 @@ class SwapTransactionSentUseCase(
) {
suspend operator fun invoke(
userWallet: UserWallet,
fromUserWallet: UserWallet,
toUserWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
toCryptoCurrencyStatus: CryptoCurrencyStatus,
fromAccount: Account?,
@ -33,7 +33,8 @@ class SwapTransactionSentUseCase(
) = Either.catch {
if (provider.type.shouldStoreSwapTransaction()) {
swapTransactionRepository.storeTransaction(
userWalletId = userWallet.walletId,
fromUserWalletId = fromUserWallet.walletId,
toUserWalletId = toUserWallet.walletId,
fromCryptoCurrency = fromCryptoCurrencyStatus.currency,
toCryptoCurrency = toCryptoCurrencyStatus.currency,
fromAccount = fromAccount,
@ -58,11 +59,11 @@ class SwapTransactionSentUseCase(
}
swapTransactionRepository.storeLastSwappedCryptoCurrencyId(
userWalletId = userWallet.walletId,
userWalletId = fromUserWallet.walletId,
cryptoCurrencyId = toCryptoCurrencyStatus.currency.id,
)
swapRepositoryV2.swapTransactionSent(
userWallet = userWallet,
userWallet = fromUserWallet,
fromCryptoCurrencyStatus = fromCryptoCurrencyStatus,
payInAddress = payInAddress,
txId = swapDataTransactionModel.txId,

View file

@ -3,14 +3,11 @@ package com.tangem.domain.tokens.actions
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@ -65,20 +62,6 @@ internal class CommonActionsFactory(
getSendUnavailabilityReason(userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus)
}
val swapUnavailabilityReason = if (!cryptoCurrencyStatus.currency.isCustom &&
cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote
) {
async {
getSwapUnavailabilityReason(
userWalletId = userWallet.walletId,
currencyStatus = cryptoCurrencyStatus,
requirementsDeferred = requirementsDeferred,
)
}
} else {
null
}
val hideTokenUnavailabilityReason = getTokenHideUnavailabilityReason(userWallet)
actionAvailabilityBuilder {
@ -111,7 +94,6 @@ internal class CommonActionsFactory(
createSwapAction(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
swapUnavailableReasonDeferred = swapUnavailabilityReason,
shouldShowSwapStories = shouldShowSwapStories,
).addByReason()
// endregion
@ -140,11 +122,9 @@ internal class CommonActionsFactory(
}
}
@Suppress("CanBeNonNullable")
private suspend fun createSwapAction(
private fun createSwapAction(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
swapUnavailableReasonDeferred: Deferred<ScenarioUnavailabilityReason>?,
shouldShowSwapStories: Boolean,
): ActionState {
val cryptoCurrency = cryptoCurrencyStatus.currency
@ -172,35 +152,11 @@ internal class CommonActionsFactory(
)
}
else -> {
val reason = requireNotNull(swapUnavailableReasonDeferred) {
"swapUnavailableReasonDeferred must not be null for available swap action"
}.await()
return ActionState.Swap(
unavailabilityReason = reason,
shouldShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories,
ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.None,
shouldShowBadge = shouldShowSwapStories,
)
}
}
}
private suspend fun getSwapUnavailabilityReason(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
requirementsDeferred: Deferred<AssetRequirementsCondition?>?,
): ScenarioUnavailabilityReason {
val swapUnavailabilityReason = rampStateManager
.availableForSwap(userWalletId = userWalletId, cryptoCurrency = currencyStatus.currency)
val shouldCheckAssetRequirements =
swapUnavailabilityReason == ScenarioUnavailabilityReason.None && requirementsDeferred != null
val yieldSupplyStatus = currencyStatus.value.yieldSupplyStatus
val isUnavailableByYieldSupply = yieldSupplyStatus?.isAllowedToSpend == false && yieldSupplyStatus.isActive
return when {
isUnavailableByYieldSupply -> ScenarioUnavailabilityReason.YieldSupplyApprovalRequired
shouldCheckAssetRequirements -> getReceiveScenario(requirementsDeferred.await())
else -> swapUnavailabilityReason
}
}
}

View file

@ -9,13 +9,32 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
class GetPaymentAccountCryptoCurrencyStatusUseCase(
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
) {
suspend operator fun invoke(
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<Pair<Account.Payment, CryptoCurrencyStatus>> {
return paymentAccountStatusSupplier(userWalletId).mapNotNull { accountStatus ->
val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) {
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
else -> return@mapNotNull null
}
if (cryptoCurrencyStatus.currency == cryptoCurrency) {
accountStatus.account to cryptoCurrencyStatus
} else {
null
}
}
}
suspend fun invokeSync(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Option<Pair<Account.Payment, CryptoCurrencyStatus>> {