Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-24 18:09:33 +03:00
commit fd0a07af67
14 changed files with 258 additions and 188 deletions

View file

@ -42,10 +42,11 @@ interface TangemExpressApi {
@Query("fromNetwork") fromNetwork: String,
@Query("toContractAddress") toContractAddress: String,
@Query("toNetwork") toNetwork: String,
@Query("fromAmount") fromAmount: BigDecimal,
@Query("providerId") providerId: Int,
@Query("rateType") rateType: RateType,
@Query("fromAmount") fromAmount: String,
@Query("providerId") providerId: String,
@Query("rateType") rateType: String,
@Query("toAddress") toAddress: String,
@Query("refundAddress") refundAddress: String,
): ApiResponse<ExchangeDataResponse>
@GET("exchange-result")

View file

@ -4,8 +4,17 @@ import com.squareup.moshi.Json
import java.math.BigDecimal
data class ExchangeDataResponse(
@Json(name = "fromAmount")
val fromAmount: String,
@Json(name = "fromDecimals")
val fromDecimals: Int,
@Json(name = "toAmount")
val toAmount: BigDecimal,
val toAmount: String,
@Json(name = "toDecimals")
val toDecimals: Int,
@Json(name = "txType")
val txType: TxType,

View file

@ -22,7 +22,6 @@ dependencies {
implementation(project(":data:common"))
implementation(project(":libs:auth"))
implementation(project(":libs:crypto"))
implementation(projects.domain.tokens.models)
implementation(deps.material)

View file

@ -49,7 +49,7 @@ internal class SwapRepositoryImpl @Inject constructor(
) : SwapRepository {
private val tokensConverter = TokensConverter()
private val swapConverter = SwapConverter()
private val expressDataConverter = ExpressDataConverter()
private val leastTokenInfoConverter = LeastTokenInfoConverter()
private val swapPairInfoConverter = SwapPairInfoConverter()
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
@ -164,8 +164,8 @@ internal class SwapRepositoryImpl @Inject constructor(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
),
)
} catch (ex: OneIncResponseException) {
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
} catch (ex: Exception) {
AggregatedSwapDataModel(null, mapErrors(ex.message))
}
}
}
@ -176,31 +176,35 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun prepareSwapTransaction(
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
fromWalletAddress: String,
slippage: Int,
override suspend fun getExchangeData(
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
toNetwork: String,
fromAmount: String,
providerId: String,
rateType: RateType,
toAddress: String,
refundAddress: String
): AggregatedSwapDataModel<SwapDataModel> {
return withContext(coroutineDispatcher.io) {
try {
val swapResponse = oneInchErrorsHandler.handleOneInchResponse(
getOneInchApi(networkId).swap(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromAddress = fromWalletAddress,
slippage = slippage,
referrerAddress = configManager.config.swapReferrerAccount?.address,
fee = configManager.config.swapReferrerAccount?.fee,
),
val response = tangemExpressApi.getExchangeData(
fromContractAddress = fromContractAddress,
fromNetwork = fromNetwork,
toContractAddress = toContractAddress,
toNetwork = toNetwork,
fromAmount = fromAmount,
providerId = providerId,
rateType = rateType.name.lowercase(),
toAddress = toAddress,
refundAddress = refundAddress
).getOrThrow()
AggregatedSwapDataModel(
dataModel = expressDataConverter.convert(response)
)
AggregatedSwapDataModel(swapConverter.convert(swapResponse))
} catch (ex: OneIncResponseException) {
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
} catch (ex: Exception) {
AggregatedSwapDataModel(null, mapErrors(ex.message))
}
}
}
@ -280,31 +284,6 @@ internal class SwapRepositoryImpl @Inject constructor(
return oneInchApiFactory.getApi(networkId)
}
override suspend fun getExchangeQuote(
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
toNetwork: String,
fromAmount: String,
providerId: String,
rateType: RateType,
): ExchangeQuote {
val response = tangemExpressApi.getExchangeQuote(
fromContractAddress,
fromNetwork,
toContractAddress,
toNetwork,
fromAmount,
providerId,
rateType.name.lowercase(),
).getOrThrow()
return ExchangeQuote(
toAmount = response.toAmount,
allowanceContract = response.allowanceContract,
)
}
override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }

View file

@ -0,0 +1,41 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.ExchangeDataResponse
import com.tangem.datasource.api.express.models.response.TxType
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.utils.converter.Converter
class ExpressDataConverter : Converter<ExchangeDataResponse, SwapDataModel> {
override fun convert(value: ExchangeDataResponse): SwapDataModel {
return SwapDataModel(
toTokenAmount = createFromAmountWithOffset(value.toAmount, value.toDecimals),
transaction = convertTransaction(value),
)
}
private fun convertTransaction(transactionDto: ExchangeDataResponse): ExpressTransactionModel {
return if (transactionDto.txType == TxType.SWAP) {
ExpressTransactionModel.DEX(
fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals),
toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals),
txId = transactionDto.txId,
txTo = transactionDto.txTo,
txFrom = requireNotNull(transactionDto.txFrom),
txData = requireNotNull(transactionDto.txData)
)
} else {
ExpressTransactionModel.CEX(
fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals),
toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals),
txId = transactionDto.txId,
txTo = transactionDto.txTo,
externalTxId = requireNotNull(transactionDto.externalTxId),
externalTxUrl = requireNotNull(transactionDto.externalTxUrl),
)
}
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.models.SwapResponse
import com.tangem.datasource.api.oneinch.models.TransactionDto
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.TransactionModel
import com.tangem.utils.converter.Converter
class SwapConverter : Converter<SwapResponse, SwapDataModel> {
override fun convert(value: SwapResponse): SwapDataModel {
return SwapDataModel(
toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals),
transaction = convertTransaction(value.transaction),
)
}
private fun convertTransaction(transactionDto: TransactionDto): TransactionModel {
return TransactionModel(
fromWalletAddress = transactionDto.fromAddress,
toWalletAddress = transactionDto.toAddress,
data = transactionDto.data,
value = transactionDto.value,
gasPrice = transactionDto.gasPrice,
gas = transactionDto.gas,
)
}
}

View file

@ -228,46 +228,114 @@ internal class SwapInteractorImpl @Inject constructor(
amountToSwap: String,
selectedFee: FeeType,
): Map<SwapProvider, SwapState> {
syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency))
val amountDecimal = toBigDecimalOrNull(amountToSwap)
if (amountDecimal == null || amountDecimal.signum() == 0) {
return providers.associateWith { createEmptyAmountState(networkId, fromToken.currency, toToken.currency) }
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val fromTokenAddress = getTokenAddress(fromToken.currency)
val toTokenAddress = getTokenAddress(toToken.currency)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId, derivationPath)
}
val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null)
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
// TODO
providers.associateWith {
loadSwapData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
fromToken = fromToken,
toToken = toToken,
amount = amount,
selectedFee = selectedFee,
)
return providers.map { provider ->
syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency))
val amountDecimal = toBigDecimalOrNull(amountToSwap)
if (amountDecimal == null || amountDecimal.signum() == 0) {
return providers.associateWith {
createEmptyAmountState(
networkId,
fromToken.currency,
toToken.currency
)
}
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val fromTokenAddress = getTokenAddress(fromToken.currency)
val toTokenAddress = getTokenAddress(toToken.currency)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId, derivationPath)
}
val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null)
when (provider.type) {
ExchangeProviderType.DEX -> {
manageDex(
networkId = networkId,
fromToken = fromToken,
toToken = toToken,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
provider = provider,
selectedFee = selectedFee,
amount = amount,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}
ExchangeProviderType.CEX -> {
manageCex(
networkId = networkId,
fromToken = fromToken,
toToken = toToken,
provider = provider,
amount = amount,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}
}
}.toMap()
}
private suspend fun manageDex(
networkId: String,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
fromTokenAddress: String,
toTokenAddress: String,
provider: SwapProvider,
selectedFee: FeeType,
amount: SwapAmount,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
): Pair<SwapProvider, SwapState> {
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
provider to loadSwapData(
provider = provider,
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
fromToken = fromToken,
toToken = toToken,
amount = amount,
selectedFee = selectedFee,
)
} else {
loadQuoteData(
provider to loadQuoteData(
networkId = networkId,
amount = amount,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
providers = providers,
provider = provider,
)
}
}
private suspend fun manageCex(
networkId: String,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
provider: SwapProvider,
amount: SwapAmount,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
): Pair<SwapProvider, SwapState> {
return provider to loadQuoteData(
networkId = networkId,
amount = amount,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
provider = provider,
)
}
@Deprecated("used in old swap mechanism")
override suspend fun onSwap(
networkId: String,
@ -286,8 +354,8 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToSend = swapCurrencyConverter.convert(currencyToSend),
feeAmount = fee.feeValue,
gasLimit = fee.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.toWalletAddress,
dataToSign = swapStateData.swapModel.transaction.data,
destinationAddress = swapStateData.swapModel.transaction.txTo,
dataToSign = (swapStateData.swapModel.transaction as ExpressTransactionModel.DEX).txData,
),
isSwap = true,
derivationPath = derivationPath,
@ -422,39 +490,33 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
fromTokenStatus: CryptoCurrencyStatus,
toTokenStatus: CryptoCurrencyStatus,
providers: List<SwapProvider>,
provider: SwapProvider,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
): Map<SwapProvider, SwapState> {
): SwapState {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
return coroutineScope {
val quoteRequests = providers.map { provider ->
async {
provider to repository.findBestQuote(
fromContractAddress = fromToken.getContractAddress(),
fromNetwork = fromToken.network.backendId,
toContractAddress = toToken.getContractAddress(),
toNetwork = toToken.network.backendId,
fromAmount = amount.toStringWithRightOffset(),
providerId = provider.providerId,
rateType = RateType.FLOAT,
)
}
}
val quotes = repository.findBestQuote(
fromContractAddress = fromToken.getContractAddress(),
fromNetwork = fromToken.network.backendId,
toContractAddress = toToken.getContractAddress(),
toNetwork = toToken.network.backendId,
fromAmount = amount.toStringWithRightOffset(),
providerId = provider.providerId,
rateType = RateType.FLOAT,
)
quoteRequests.awaitAll().map {
it.first to
getState(
quoteDataModel = it.second,
amount = amount,
fromToken = fromTokenStatus,
toToken = toTokenStatus,
networkId = networkId,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}.associate { it.first to it.second }
getState(
quoteDataModel = quotes,
amount = amount,
fromToken = fromTokenStatus,
toToken = toTokenStatus,
networkId = networkId,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}
}
@ -510,6 +572,7 @@ internal class SwapInteractorImpl @Inject constructor(
*/
@Suppress("LongParameterList")
private suspend fun loadSwapData(
provider: SwapProvider,
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
@ -518,13 +581,16 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
selectedFee: FeeType,
): SwapState {
repository.prepareSwapTransaction(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount.toStringWithRightOffset(),
slippage = DEFAULT_SLIPPAGE,
fromWalletAddress = getWalletAddress(networkId),
repository.getExchangeData(
fromContractAddress = fromToken.currency.getContractAddress(),
fromNetwork = fromToken.currency.network.backendId,
toContractAddress = toToken.currency.getContractAddress(),
toNetwork = toToken.currency.network.backendId,
fromAmount = amount.toStringWithRightOffset(),
providerId = provider.providerId,
rateType = RateType.FLOAT,
toAddress = toToken.value.networkAddress?.defaultAddress ?: "",
refundAddress = fromToken.value.networkAddress?.defaultAddress ?: "",
).let {
val swapData = it.dataModel
if (swapData != null) {
@ -532,9 +598,9 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
amountToSend = amount.value,
currencyToSend = swapCurrencyConverter.convert(fromToken.currency),
destinationAddress = swapData.transaction.toWalletAddress,
destinationAddress = swapData.transaction.txTo,
increaseBy = INCREASE_GAS_LIMIT_BY,
data = swapData.transaction.data,
data = (swapData.transaction as ExpressTransactionModel.DEX).txData,
derivationPath = derivationPath,
)
val txFeeState = proxyFeesToFeeState(networkId, feeData)

View file

@ -32,16 +32,6 @@ interface SwapRepository {
*/
suspend fun addressForTrust(networkId: String): String
@Suppress("LongParameterList")
suspend fun prepareSwapTransaction(
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
fromWalletAddress: String,
slippage: Int,
): AggregatedSwapDataModel<SwapDataModel>
/**
* Returns a tangem fee for swap in percents
* Example: 0.35%
@ -67,7 +57,7 @@ interface SwapRepository {
): String
@Suppress("LongParameterList")
suspend fun getExchangeQuote(
suspend fun getExchangeData(
fromContractAddress: String,
fromNetwork: String,
toContractAddress: String,
@ -75,7 +65,9 @@ interface SwapRepository {
fromAmount: String,
providerId: String,
rateType: RateType,
): ExchangeQuote
toAddress: String,
refundAddress: String,
): AggregatedSwapDataModel<SwapDataModel>
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
}

View file

@ -1,20 +1,10 @@
package com.tangem.feature.swap.domain.models
sealed class DataError {
object NoError : DataError()
data class UnknownError(val message: String) : DataError()
object InsufficientLiquidity : DataError()
object UnknownError : DataError()
data class Error(val message: String) : DataError()
}
fun mapErrors(error: String?): DataError {
return if (error == null) {
DataError.NoError
} else {
when (error) {
INSUFFICIENT_LIQUIDITY_ERROR -> DataError.InsufficientLiquidity
else -> DataError.UnknownError(error)
}
}
}
private const val INSUFFICIENT_LIQUIDITY_ERROR = "insufficient liquidity"
return error?.let { DataError.Error(it) } ?: DataError.UnknownError
}

View file

@ -11,5 +11,5 @@ import com.tangem.feature.swap.domain.models.DataError
*/
data class AggregatedSwapDataModel<T>(
val dataModel: T?,
val error: DataError = DataError.NoError,
val error: DataError = DataError.UnknownError,
)

View file

@ -0,0 +1,30 @@
package com.tangem.feature.swap.domain.models.domain
import com.tangem.feature.swap.domain.models.SwapAmount
sealed class ExpressTransactionModel {
abstract val fromAmount: SwapAmount
abstract val toAmount: SwapAmount
abstract val txId: String
abstract val txTo: String
data class DEX(
override val fromAmount: SwapAmount,
override val toAmount: SwapAmount,
override val txId: String,
override val txTo: String,
val txFrom: String,
val txData: String,
): ExpressTransactionModel()
data class CEX(
override val fromAmount: SwapAmount,
override val toAmount: SwapAmount,
override val txId: String,
override val txTo: String,
val externalTxId: String,
val externalTxUrl: String,
): ExpressTransactionModel()
}

View file

@ -2,13 +2,7 @@ package com.tangem.feature.swap.domain.models.domain
import com.tangem.feature.swap.domain.models.SwapAmount
/**
* Swap transaction model
*
* @property toTokenAmount amount "target" token
* @property transaction info about transaction
*/
data class SwapDataModel(
val toTokenAmount: SwapAmount,
val transaction: TransactionModel,
val transaction: ExpressTransactionModel,
)

View file

@ -574,7 +574,7 @@ internal class StateBuilder(
// todo use if needed later
// DataError.InsufficientLiquidity -> TODO()
// DataError.NoError -> TODO()
is DataError.UnknownError -> addWarning(uiState, error.message, true, onClick)
is DataError.Error -> addWarning(uiState, error.message, true, onClick)
else -> addWarning(uiState, null, false) {}
}
}

View file

@ -1,12 +1,10 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
/** Coroutines */
implementation(deps.kotlin.coroutines)
implementation(projects.domain.tokens.models)
}