Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-14 14:22:43 +02:00
parent 95f66c8798
commit cfdcf4da41
16 changed files with 278 additions and 20 deletions

View file

@ -19,8 +19,8 @@ data class SwapPairProvider(
@Json(name = "providerId")
val providerId: Int,
@Json(name = "rateType")
val rateType: RateType,
@Json(name = "rateTypes")
val rateTypes: List<RateType>,
)
enum class RateType {

View file

@ -1,9 +1,7 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.local.token.AssetsStore
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId
@ -13,14 +11,11 @@ class DefaultMarketCryptoCurrencyRepository(
) : MarketCryptoCurrencyRepository {
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
val cryptoCurrencyId = cryptoCurrency.id
val blockchain = Blockchain.fromId(cryptoCurrencyId.rawNetworkId)
val apiNetworkId = blockchain.toNetworkId()
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
return assetsStore.getSyncOrNull(userWalletId)?.find {
it.network == apiNetworkId &&
it.token == cryptoCurrencyId.rawCurrencyId &&
it.network == cryptoCurrency.network.backendId &&
it.token == cryptoCurrency.id.rawCurrencyId &&
it.contractAddress == contractAddress &&
it.isActive
}?.exchangeAvailable ?: false

View file

@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
@ -21,6 +22,7 @@ internal fun getNetwork(
return Network(
id = Network.ID(blockchain.id),
backendId = blockchain.toNetworkId(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider),

View file

@ -11,6 +11,7 @@ import kotlinx.parcelize.Parcelize
* (e.g., ERC20, BEP20).
*
* @property id The unique identifier of the network.
* @property backendId The name of this network in the Tangem backend.
* @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin".
* @property derivationPath The path used to derive keys for this network.
* @property isTestnet Indicates whether the network is a test network or a main network.
@ -19,6 +20,7 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class Network(
val id: ID,
val backendId: String,
val name: String,
val derivationPath: DerivationPath,
val isTestnet: Boolean,

View file

@ -6,6 +6,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
@ -19,23 +22,23 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.QuotesConverter
import com.tangem.feature.swap.converters.SwapConverter
import com.tangem.feature.swap.converters.TokensConverter
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.QuoteModel
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.mapErrors
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo
@Suppress("LongParameterList")
internal class SwapRepositoryImpl @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val oneInchApiFactory: OneInchApiFactory,
private val oneInchErrorsHandler: OneInchErrorsHandler,
private val coroutineDispatcher: CoroutineDispatcherProvider,
@ -46,6 +49,51 @@ internal class SwapRepositoryImpl @Inject constructor(
private val tokensConverter = TokensConverter()
private val quotesConverter = QuotesConverter()
private val swapConverter = SwapConverter()
private val leastTokenInfoConverter = LeastTokenInfoConverter()
private val swapPairInfoConverter = SwapPairInfoConverter()
override suspend fun getPairs(
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
): List<SwapPairLeast> {
return withContext(coroutineDispatcher.io) {
val initial = NetworkLeastTokenInfo(
contractAddress = initialCurrency.contractAddress,
network = initialCurrency.network,
)
val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }
val pairs = async {
getPairsInternal(
from = arrayListOf(initial),
to = currenciesList,
)
}
val reversedPairs = async {
getPairsInternal(
from = currenciesList,
to = arrayListOf(initial),
)
}
pairs.await() + reversedPairs.await()
}
}
private suspend fun getPairsInternal(
from: List<NetworkLeastTokenInfo>,
to: List<NetworkLeastTokenInfo>,
): List<SwapPairLeast> {
return tangemExpressApi.getPairs(
PairsRequestBody(
from = from,
to = to,
),
)
.getOrThrow()
.map { swapPairInfoConverter.convert(it) }
}
override suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double> {
// workaround cause backend do not return arbitrum and optimism rates

View file

@ -0,0 +1,17 @@
package com.tangem.feature.swap.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.converter.Converter
class LeastTokenInfoConverter : Converter<CryptoCurrency, LeastTokenInfo> {
override fun convert(value: CryptoCurrency): LeastTokenInfo {
return LeastTokenInfo(
contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0",
network = Blockchain.fromId(value.id.rawNetworkId).toNetworkId(),
)
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.RateType
import com.tangem.datasource.api.express.models.response.SwapPair
import com.tangem.datasource.api.express.models.response.SwapPairProvider
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain
import com.tangem.feature.swap.domain.models.domain.SwapPairProvider as SwapPairProviderDomain
import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain
import com.tangem.utils.converter.Converter
class SwapPairInfoConverter : Converter<SwapPair, SwapPairDomain> {
override fun convert(value: SwapPair): SwapPairDomain {
return SwapPairDomain(
from = LeastTokenInfo(
contractAddress = value.from.contractAddress,
network = value.from.network,
),
to = LeastTokenInfo(
contractAddress = value.to.contractAddress,
network = value.to.network,
),
providers = value.providers.map {
convertProvider(it)
},
)
}
private fun convertProvider(swapPairProvider: SwapPairProvider): SwapPairProviderDomain {
return SwapPairProviderDomain(
providerId = swapPairProvider.providerId,
rateTypes = swapPairProvider.rateTypes.map { convertRateType(it) },
)
}
private fun convertRateType(rateType: RateType): RateTypeDomain {
return when (rateType) {
RateType.FIXED -> RateTypeDomain.FIXED
RateType.FLOAT -> RateTypeDomain.FLOAT
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.di
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -22,6 +23,7 @@ class SwapDataModule {
@Singleton
fun provideSwapRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
oneInchApiFactory: OneInchApiFactory,
oneInchErrorsHandler: OneInchErrorsHandler,
coroutineDispatcher: CoroutineDispatcherProvider,
@ -30,6 +32,7 @@ class SwapDataModule {
): SwapRepository {
return SwapRepositoryImpl(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
oneInchApiFactory = oneInchApiFactory,
oneInchErrorsHandler = oneInchErrorsHandler,
coroutineDispatcher = coroutineDispatcher,

View file

@ -1,13 +1,17 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
interface SwapInteractor {
suspend fun getPairs(currency: Currency): List<SwapPair>
suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List<CryptoCurrency>): List<SwapPairLeast>
fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?)
/**
@ -17,6 +21,7 @@ interface SwapInteractor {
* @return [TokensDataState] that contains info about all available to swap tokens for networkId
* and preselected tokens which initially select to swap
*/
@Deprecated("method is used in the old swap mechanism")
suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState
/**

View file

@ -1,5 +1,7 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -34,6 +36,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val networksRepository: NetworksRepository,
private val walletFeatureToggles: WalletFeatureToggles,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
) : SwapInteractor {
private val swapCurrencyConverter = SwapCurrencyConverter()
@ -41,6 +44,64 @@ internal class SwapInteractorImpl @Inject constructor(
private var derivationPath: String? = null
private var network: Network? = null
override suspend fun getPairs(currency: Currency): List<SwapPair> {
val currencies = getSelectedWalletSyncUseCase().fold(
ifLeft = { emptyList() },
ifRight = { selectedWallet ->
getCryptoCurrenciesUseCase(selectedWallet.walletId).fold(
ifLeft = { emptyList() },
ifRight = { it },
)
},
)
val pairs = getPairs(
initialCurrency = LeastTokenInfo(
contractAddress = (currency as? Currency.NonNativeToken)?.contractAddress ?: "0",
network = currency.networkId,
),
currenciesList = currencies,
)
return createCryptoCurrencyPairs(pairs, currencies)
}
private fun createCryptoCurrencyPairs(
swapPairLeasts: List<SwapPairLeast>,
cryptoCurrenciesList: List<CryptoCurrency>,
): List<SwapPair> {
return swapPairLeasts.mapNotNull {
val from = findCryptoCurrencyByLeastInfo(it.from, cryptoCurrenciesList)
val to = findCryptoCurrencyByLeastInfo(it.to, cryptoCurrenciesList)
if (from != null && to != null) {
SwapPair(
from = from,
to = to,
providers = it.providers,
)
} else {
null
}
}
}
private fun findCryptoCurrencyByLeastInfo(
leastTokenInfo: LeastTokenInfo,
cryptoCurrenciesList: List<CryptoCurrency>,
): CryptoCurrency? {
return cryptoCurrenciesList.find {
it.network.backendId == leastTokenInfo.network &&
(it as? CryptoCurrency.Token)?.contractAddress ?: "0" == leastTokenInfo.contractAddress
}
}
override suspend fun getPairs(
initialCurrency: LeastTokenInfo,
currenciesList: List<CryptoCurrency>,
): List<SwapPairLeast> {
return repository.getPairs(initialCurrency, currenciesList)
}
override fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?) {
this.derivationPath = derivationPath
this.network = network

View file

@ -5,13 +5,13 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.QuoteModel
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.*
import java.math.BigDecimal
interface SwapRepository {
suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List<CryptoCurrency>): List<SwapPairLeast>
suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double>
suspend fun getExchangeableTokens(networkId: String): List<Currency>

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain.di
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.legacy.WalletsStateHolder
@ -30,6 +31,7 @@ class SwapDomainModule {
networksRepository: NetworksRepository,
walletFeatureToggles: WalletFeatureToggles,
@SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
@SwapScope getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@ -41,6 +43,7 @@ class SwapDomainModule {
networksRepository = networksRepository,
walletFeatureToggles = walletFeatureToggles,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getCryptoCurrenciesUseCase = getCryptoCurrenciesUseCase,
)
}
@ -58,6 +61,13 @@ class SwapDomainModule {
fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder)
}
@SwapScope
@Provides
@Singleton
fun providesGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
}
}
@Qualifier

View file

@ -0,0 +1,6 @@
package com.tangem.feature.swap.domain.models.domain
data class LeastTokenInfo(
val contractAddress: String,
val network: String,
)

View file

@ -0,0 +1,50 @@
package com.tangem.feature.swap.domain.models.domain
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Domain layer representation of SwapPair data network model.
*
* @property from Short information about the token we want to change
* @property to Short information about the token we want to exchange for
* @property providers Exchange providers
*/
data class SwapPairLeast(
val from: LeastTokenInfo,
val to: LeastTokenInfo,
val providers: List<SwapPairProvider>,
)
/**
* Enriched model of swap pair data. Contains full CryptoCurrency models instead of least info.
*
* @property from CryptoCurrency we want to change
* @property to CryptoCurrency we want to exchange for
* @property providers Exchange providers
*/
data class SwapPair(
val from: CryptoCurrency,
val to: CryptoCurrency,
val providers: List<SwapPairProvider>,
)
/**
* Provider that could swap given cryptocurrencies
*
* @property providerId provider id
* @property rateTypes supported rate types
*/
data class SwapPairProvider(
val providerId: Int,
val rateTypes: List<RateType>,
)
/**
* Rate type.
*
* Current implementation contains only float type, fixed will be supported later.
*/
enum class RateType {
FLOAT,
FIXED,
}

View file

@ -1,14 +1,21 @@
package com.tangem.feature.swap.viewmodels
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TxFee
data class SwapProcessDataState(
// Initial network id
val networkId: String,
@Deprecated("used in old swap mechanism")
val fromCurrency: Currency? = null,
@Deprecated("used in old swap mechanism")
val toCurrency: Currency? = null,
val fromCryptoCurrency: CryptoCurrency? = null,
val toCryptoCurrency: CryptoCurrency? = null,
// Amount from input
val amount: String? = null,
val approveDataModel: RequestApproveStateData? = null,
val swapDataModel: SwapStateData? = null,

View file

@ -137,6 +137,15 @@ internal class SwapViewModel @Inject constructor(
}
private fun initTokens(currency: Currency) {
// new flow
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
val pairs = swapInteractor.getPairs(currency)
// TODO
}
}
// old flow
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
swapInteractor.initTokensToSwap(currency)