Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-12 19:57:17 +03:00
commit f99ac86338
45 changed files with 165 additions and 123 deletions

View file

@ -87,6 +87,7 @@ dependencies {
implementation(projects.features.swap.api)
implementation(projects.features.swap.presentation)
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.data)
implementation(projects.features.tester.api)
implementation(projects.features.tester.impl)

View file

@ -15,6 +15,8 @@ dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.models)
implementation(projects.features.swap.domain.api)
/** Network */
implementation(deps.retrofit)

View file

@ -1,8 +1,10 @@
package com.tangem.feature.swap
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.right
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Approver
import com.tangem.blockchain.common.Blockchain
@ -27,12 +29,11 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
@ -184,7 +185,7 @@ internal class SwapRepositoryImpl @Inject constructor(
toDecimals: Int,
providerId: String,
rateType: RateType,
): AggregatedSwapDataModel<QuoteModel> {
): Either<DataError, QuoteModel> {
return withContext(coroutineDispatcher.io) {
try {
val response = tangemExpressApi.getExchangeQuote(
@ -198,14 +199,12 @@ internal class SwapRepositoryImpl @Inject constructor(
providerId = providerId,
rateType = rateType.name.lowercase(),
).getOrThrow()
AggregatedSwapDataModel(
dataModel = QuoteModel(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
allowanceContract = response.allowanceContract,
),
)
QuoteModel(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
allowanceContract = response.allowanceContract,
).right()
} catch (ex: Exception) {
AggregatedSwapDataModel(null, getDataError(ex))
getDataError(ex).left()
}
}
}
@ -227,7 +226,7 @@ internal class SwapRepositoryImpl @Inject constructor(
providerId: String,
rateType: RateType,
toAddress: String,
): AggregatedSwapDataModel<SwapDataModel> {
): Either<DataError, SwapDataModel> {
return withContext(coroutineDispatcher.io) {
try {
val response = tangemExpressApi.getExchangeData(
@ -242,11 +241,9 @@ internal class SwapRepositoryImpl @Inject constructor(
rateType = rateType.name.lowercase(),
toAddress = toAddress,
).getOrThrow()
AggregatedSwapDataModel(
dataModel = expressDataConverter.convert(response),
)
expressDataConverter.convert(response).right()
} catch (ex: Exception) {
AggregatedSwapDataModel(null, getDataError(ex))
getDataError(ex).left()
}
}
}

View file

@ -13,7 +13,7 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.converters.ErrorsDataConverter
import com.tangem.feature.swap.DefaultSwapTransactionRepository
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module

1
features/swap/domain/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,18 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.feature.swap.domain.api"
}
dependencies {
implementation(projects.features.swap.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(deps.arrow.core)
}

View file

@ -1,10 +1,10 @@
package com.tangem.feature.swap.domain
package com.tangem.feature.swap.domain.api
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.DataError
import java.math.BigDecimal
interface SwapRepository {
@ -28,7 +28,7 @@ interface SwapRepository {
toDecimals: Int,
providerId: String,
rateType: RateType,
): AggregatedSwapDataModel<QuoteModel>
): Either<DataError, QuoteModel>
/**
* Returns address of 1inch router that must be trusted
@ -77,7 +77,7 @@ interface SwapRepository {
providerId: String,
rateType: RateType,
toAddress: String,
): AggregatedSwapDataModel<SwapDataModel>
): Either<DataError, SwapDataModel>
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
}

View file

@ -29,6 +29,8 @@ dependencies {
implementation(projects.domain.card)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.txhistory.models)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** Core modules */
implementation(projects.core.utils)
@ -38,7 +40,6 @@ dependencies {
implementation(projects.features.wallet.api)
/** Other Libraries **/
implementation(deps.kotlin.serialization)
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.timber)

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,23 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.feature.swap.domain.models"
}
dependencies {
/** Domain */
implementation(projects.domain.tokens.models)
/** Core modules */
implementation(projects.core.utils)
/** Other Libraries **/
implementation(deps.kotlin.serialization)
implementation(deps.arrow.core)
}

View file

@ -5,7 +5,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.*
interface SwapInteractor {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
@ -19,9 +20,10 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.*
@ -297,13 +299,14 @@ internal class SwapInteractorImpl @Inject constructor(
)
val fromTokenAddress = getTokenAddress(fromToken.currency)
val isAllowedToSpend = if (quotes.dataModel != null) {
quotes.dataModel.allowanceContract?.let {
isAllowedToSpend(networkId, fromToken.currency, amount, it)
} ?: true
} else {
false
}
val isAllowedToSpend = quotes.fold(
ifRight = {
it.allowanceContract?.let {
isAllowedToSpend(networkId, fromToken.currency, amount, it)
} ?: true
},
ifLeft = { false },
)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
@ -497,13 +500,13 @@ internal class SwapInteractorImpl @Inject constructor(
providerId = swapProvider.providerId,
rateType = RateType.FLOAT,
toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "",
)
).getOrNull()
val txData = walletManagersFacade.createTransaction(
amount = amount.value.convertToAmount(currencyToSend.currency),
fee = getFeeForTransaction(txFee),
memo = null,
destination = (exchangeData.dataModel?.transaction as ExpressTransactionModel.CEX).txTo,
destination = (exchangeData?.transaction as ExpressTransactionModel.CEX).txTo,
userWalletId = userWalletId,
network = currencyToSend.currency.network,
)
@ -514,7 +517,7 @@ internal class SwapInteractorImpl @Inject constructor(
network = currencyToSend.currency.network,
)
val externalUrl = (exchangeData.dataModel.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl
val externalUrl = (exchangeData.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl
return result.fold(
ifLeft = {
@ -533,7 +536,7 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToGet = currencyToGet,
amount = amount,
swapProvider = swapProvider,
swapDataModel = exchangeData.dataModel,
swapDataModel = exchangeData,
timestamp = timestamp,
)
storeLastCryptoCurrencyId(currencyToGet.currency)
@ -544,10 +547,10 @@ internal class SwapInteractorImpl @Inject constructor(
),
fromAmountValue = amount.value,
toAmount = amountFormatter.formatSwapAmountToUI(
exchangeData.dataModel.toTokenAmount,
exchangeData.toTokenAmount,
currencyToGet.currency.symbol,
),
toAmountValue = exchangeData.dataModel.toTokenAmount.value,
toAmountValue = exchangeData.toTokenAmount.value,
txAddress = userWalletManager.getLastTransactionHash(
currencyToSend.currency.network.backendId,
derivationPath,
@ -751,7 +754,7 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun getQuotesState(
exchangeProviderType: ExchangeProviderType,
quoteDataModel: AggregatedSwapDataModel<QuoteModel>,
quoteDataModel: Either<DataError, QuoteModel>,
amount: SwapAmount,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
@ -761,58 +764,60 @@ internal class SwapInteractorImpl @Inject constructor(
txFee: TxFeeState,
includeFeeInAmount: IncludeFeeInAmount,
): SwapState {
val quoteModel = quoteDataModel.dataModel
if (quoteModel != null) {
val swapState = updateBalances(
networkId = networkId,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
fromTokenAmount = amount,
toTokenAmount = quoteModel.toTokenAmount,
swapData = null,
txFeeState = txFee,
)
return quoteDataModel.fold(
ifRight = { quoteModel ->
val swapState = updateBalances(
networkId = networkId,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
fromTokenAmount = amount,
toTokenAmount = quoteModel.toTokenAmount,
swapData = null,
txFeeState = txFee,
)
return when (exchangeProviderType) {
ExchangeProviderType.DEX -> {
val state = updatePermissionState(
networkId = networkId,
fromTokenStatus = fromToken,
swapAmount = amount,
quotesLoadedState = swapState,
isAllowedToSpend = isAllowedToSpend,
spenderAddress = quoteModel.allowanceContract,
)
state.copy(
preparedSwapConfigState = state.preparedSwapConfigState.copy(
when (exchangeProviderType) {
ExchangeProviderType.DEX -> {
val state = updatePermissionState(
networkId = networkId,
fromTokenStatus = fromToken,
swapAmount = amount,
quotesLoadedState = swapState,
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
),
)
spenderAddress = quoteModel.allowanceContract,
)
state.copy(
preparedSwapConfigState = state.preparedSwapConfigState.copy(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
),
)
}
ExchangeProviderType.CEX -> {
swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
includeFeeInAmount = includeFeeInAmount,
),
)
}
}
ExchangeProviderType.CEX -> {
swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
includeFeeInAmount = includeFeeInAmount,
),
)
}
}
} else {
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
tokenAmount = amount,
amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
)
return SwapState.SwapError(fromTokenSwapInfo, quoteDataModel.error)
}
},
ifLeft = { error ->
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
tokenAmount = amount,
amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
)
return SwapState.SwapError(fromTokenSwapInfo, error)
},
)
}
private suspend fun getIncludeFeeInAmount(
@ -882,7 +887,7 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
selectedFee: FeeType,
): SwapState {
repository.getExchangeData(
return repository.getExchangeData(
fromContractAddress = fromToken.currency.getContractAddress(),
fromNetwork = fromToken.currency.network.backendId,
toContractAddress = toToken.currency.getContractAddress(),
@ -893,9 +898,8 @@ internal class SwapInteractorImpl @Inject constructor(
providerId = provider.providerId,
rateType = RateType.FLOAT,
toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "",
).let {
val swapData = it.dataModel
if (swapData != null) {
).fold(
ifRight = { swapData ->
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = amount.value,
@ -926,7 +930,7 @@ internal class SwapInteractorImpl @Inject constructor(
swapData = swapData,
txFeeState = txFeeState,
)
return swapState.copy(
swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = true,
@ -936,7 +940,8 @@ internal class SwapInteractorImpl @Inject constructor(
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
),
)
} else {
},
ifLeft = { error ->
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
tokenAmount = amount,
@ -944,12 +949,12 @@ internal class SwapInteractorImpl @Inject constructor(
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
)
return SwapState.SwapError(
SwapState.SwapError(
fromTokenSwapInfo,
it.error,
error,
)
}
}
},
)
}
@Suppress("LongParameterList")

View file

@ -13,6 +13,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.*
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider

View file

@ -1,15 +0,0 @@
package com.tangem.feature.swap.domain.models.data
import com.tangem.feature.swap.domain.models.DataError
/**
* Model that aggregate data model from repository return with error [DataError] if it exists
*
* @param T model type
* @property dataModel
* @property error possible from repository [DataError]
*/
data class AggregatedSwapDataModel<T>(
val dataModel: T?,
val error: DataError = DataError.UnknownError,
)

View file

@ -26,6 +26,10 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** AndroidX */
implementation(deps.androidx.activity.compose)
@ -48,11 +52,6 @@ dependencies {
implementation(projects.features.swap.api)
implementation(projects.features.tokendetails.api)
/** Domain */
implementation(projects.features.swap.domain)
implementation(projects.domain.tokens.models)
implementation(projects.domain.settings)
/** Other libraries */
implementation(deps.compose.shimmer)
implementation(deps.kotlin.serialization)

View file

@ -12,11 +12,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.*
import com.tangem.feature.swap.presentation.R

View file

@ -22,8 +22,8 @@ import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.feature.swap.router.SwapNavScreen

View file

@ -70,9 +70,10 @@ dependencies {
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** Feature Apis */
implementation(projects.features.tokendetails.api)
implementation(projects.features.send.api)
implementation(projects.features.swap.domain)
}

View file

@ -12,7 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel

View file

@ -35,7 +35,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState

View file

@ -79,12 +79,16 @@ include(":libs:auth")
// region Feature modules
include(":features:onboarding")
include(":features:referral:data")
include(":features:referral:domain")
include(":features:referral:presentation")
include(":features:swap:api")
include(":features:swap:data")
include(":features:swap:domain")
include(":features:swap:domain:models")
include(":features:swap:domain:api")
include(":features:swap:presentation")
include(":features:tester:api")