Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-27 18:33:34 +03:00
parent 3416b28b13
commit 2a5649c941
13 changed files with 101 additions and 62 deletions

View file

@ -161,6 +161,7 @@ internal class SwapRepositoryImpl @Inject constructor(
AggregatedSwapDataModel(
dataModel = QuoteModel(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
allowanceContract = response.allowanceContract,
),
)
} catch (ex: Exception) {
@ -218,6 +219,7 @@ internal class SwapRepositoryImpl @Inject constructor(
derivationPath: String?,
tokenDecimalCount: Int,
tokenAddress: String,
spenderAddress: String,
): BigDecimal {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
@ -225,7 +227,6 @@ internal class SwapRepositoryImpl @Inject constructor(
blockchain = blockchain,
derivationPath = derivationPath,
)
val spenderAddress = addressForTrust(networkId)
val result = (walletManager as? Approver)?.getAllowance(
spenderAddress,
@ -248,6 +249,7 @@ internal class SwapRepositoryImpl @Inject constructor(
derivationPath: String?,
currency: CryptoCurrency,
amount: BigDecimal?,
spenderAddress: String,
): String {
val blockchain =
requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
@ -256,7 +258,6 @@ internal class SwapRepositoryImpl @Inject constructor(
blockchain = blockchain,
derivationPath = derivationPath,
)
val spenderAddress = addressForTrust(networkId)
return (walletManager as? Approver)?.getApproveData(
spenderAddress,

View file

@ -1,15 +0,0 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.models.QuoteResponse
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.QuoteModel
import com.tangem.utils.converter.Converter
class QuotesConverter : Converter<QuoteResponse, QuoteModel> {
override fun convert(value: QuoteResponse): QuoteModel {
return QuoteModel(
toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals),
)
}
}

View file

@ -197,6 +197,7 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
derivationPath = derivationPath,
fromToken = permissionOptions.fromToken,
spenderAddress = permissionOptions.spenderAddress,
)
} else {
permissionOptions.approveData.approveData
@ -289,8 +290,22 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
isBalanceWithoutFeeEnough: Boolean,
): Pair<SwapProvider, SwapState> {
val quotes = repository.findBestQuote(
fromContractAddress = fromToken.currency.getContractAddress(),
fromNetwork = fromToken.currency.network.backendId,
toContractAddress = toToken.currency.getContractAddress(),
toNetwork = toToken.currency.network.backendId,
fromAmount = amount.toStringWithRightOffset(),
fromDecimals = amount.decimals,
providerId = provider.providerId,
rateType = RateType.FLOAT,
)
val fromTokenAddress = getTokenAddress(fromToken.currency)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount)
val isAllowedToSpend = quotes.dataModel?.allowanceContract?.let {
isAllowedToSpend(networkId, fromToken.currency, amount, it)
} ?: false
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId, derivationPath)
@ -305,15 +320,16 @@ internal class SwapInteractorImpl @Inject constructor(
selectedFee = selectedFee,
)
} else {
provider to loadQuoteData(
provider to getQuotesState(
exchangeProviderType = ExchangeProviderType.DEX,
networkId = networkId,
quoteDataModel = quotes,
amount = amount,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
fromToken = fromToken,
toToken = toToken,
networkId = networkId,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
provider = provider,
providerType = provider.type,
selectedFee = selectedFee,
)
}
@ -501,16 +517,22 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun isAllowedToSpend(networkId: String, fromToken: CryptoCurrency, amount: SwapAmount): Boolean {
private suspend fun isAllowedToSpend(
networkId: String,
fromToken: CryptoCurrency,
amount: SwapAmount,
spenderAddress: String,
): Boolean {
if (fromToken is CryptoCurrency.Coin) return true
return getSelectedWalletSyncUseCase().fold(
ifRight = { userWallet ->
val allowance = repository.getAllowance(
userWallet.walletId,
networkId,
derivationPath,
getTokenDecimals(fromToken),
getTokenAddress(fromToken),
userWalletId = userWallet.walletId,
networkId = networkId,
derivationPath = derivationPath,
tokenDecimalCount = getTokenDecimals(fromToken),
tokenAddress = getTokenAddress(fromToken),
spenderAddress = spenderAddress,
)
allowance >= amount.value
},
@ -620,6 +642,8 @@ internal class SwapInteractorImpl @Inject constructor(
fromToken = fromToken.currency,
swapAmount = amount,
quotesLoadedState = swapState,
isAllowedToSpend = isAllowedToSpend,
spenderAddress = requireNotNull(quoteModel.allowanceContract) { "Allowance contract is null" },
)
state.copy(
preparedSwapConfigState = state.preparedSwapConfigState.copy(
@ -791,19 +815,26 @@ internal class SwapInteractorImpl @Inject constructor(
},
ifRight = { txFee ->
txFee.toTxFeeState(networkId)
}
},
) ?: TxFeeState.Empty
}
return TxFeeState.Empty
}
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
private suspend fun updatePermissionState(
networkId: String,
fromToken: CryptoCurrency,
swapAmount: SwapAmount,
quotesLoadedState: SwapState.QuotesLoadedState,
spenderAddress: String,
isAllowedToSpend: Boolean,
): SwapState.QuotesLoadedState {
if (isAllowedToSpend) {
return quotesLoadedState.copy(
permissionState = PermissionDataState.Empty,
)
}
// if token balance ZERO not show permission state to avoid user to spend money for fee
val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.signum() == 0
if (isTokenZeroBalance) {
@ -822,20 +853,28 @@ internal class SwapInteractorImpl @Inject constructor(
derivationPath = derivationPath,
fromToken = fromToken,
swapAmount = swapAmount,
spenderAddress = spenderAddress,
)
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
destinationAddress = getTokenAddress(fromToken),
increaseBy = INCREASE_GAS_LIMIT_BY,
data = transactionData,
derivationPath = derivationPath,
)
val feeState = when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId)
val feeData = try {
transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
destinationAddress = fromToken.getContractAddress(),
increaseBy = INCREASE_GAS_LIMIT_BY,
data = transactionData,
derivationPath = derivationPath,
)
} catch (e: Exception) {
Timber.e(e, "Failed to get fee")
null
}
val feeState = feeData?.let {
when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId)
}
} ?: TxFeeState.Empty
val fee = when (feeState) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue
@ -857,6 +896,7 @@ internal class SwapInteractorImpl @Inject constructor(
fee = feeState,
approveData = transactionData,
fromTokenAmount = swapAmount,
spenderAddress = spenderAddress,
),
),
preparedSwapConfigState = quotesLoadedState.preparedSwapConfigState.copy(
@ -1089,6 +1129,7 @@ internal class SwapInteractorImpl @Inject constructor(
derivationPath: String?,
fromToken: CryptoCurrency,
swapAmount: SwapAmount? = null,
spenderAddress: String,
): String {
return getSelectedWalletSyncUseCase().fold(
ifRight = { userWallet ->
@ -1098,6 +1139,7 @@ internal class SwapInteractorImpl @Inject constructor(
derivationPath = derivationPath,
currency = fromToken,
amount = swapAmount?.value,
spenderAddress = spenderAddress,
)
},
ifLeft = {

View file

@ -39,6 +39,7 @@ interface SwapRepository {
*/
fun getTangemFee(): Double
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun getAllowance(
userWalletId: UserWalletId,
@ -46,8 +47,10 @@ interface SwapRepository {
derivationPath: String?,
tokenDecimalCount: Int,
tokenAddress: String,
spenderAddress: String,
): BigDecimal
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun getApproveData(
userWalletId: UserWalletId,
@ -55,6 +58,7 @@ interface SwapRepository {
derivationPath: String?,
currency: CryptoCurrency,
amount: BigDecimal?,
spenderAddress: String,
): String
@Suppress("LongParameterList")

View file

@ -17,6 +17,7 @@ data class PermissionOptions(
val approveData: RequestApproveStateData,
val forTokenContractAddress: String,
val fromToken: CryptoCurrency,
val spenderAddress: String,
val approveType: SwapApproveType,
val txFee: TxFee,
)

View file

@ -9,4 +9,5 @@ import com.tangem.feature.swap.domain.models.SwapAmount
*/
data class QuoteModel(
val toTokenAmount: SwapAmount,
val allowanceContract: String?,
)

View file

@ -61,6 +61,7 @@ data class RequestApproveStateData(
val fee: TxFeeState,
val approveData: String,
val fromTokenAmount: SwapAmount,
val spenderAddress: String,
)
// data class SwapStateData(

View file

@ -36,7 +36,6 @@ data class SwapStateHolder(
val onSuccess: (() -> Unit)? = null,
val onMaxAmountSelected: (() -> Unit)? = null,
val onShowPermissionBottomSheet: () -> Unit = {},
val onCancelPermissionBottomSheet: () -> Unit = {},
)
sealed class SwapCardState {

View file

@ -13,7 +13,6 @@ data class UiActions(
val onBackClicked: () -> Unit,
val onMaxAmountSelected: () -> Unit,
val openPermissionBottomSheet: () -> Unit,
val hidePermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
// region new actions
val onClickFee: () -> Unit,

View file

@ -3,7 +3,7 @@ package com.tangem.feature.swap.models.states
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.feature.swap.models.SwapPermissionState
class GivePermissionBottomSheetConfig(
data class GivePermissionBottomSheetConfig(
val data: SwapPermissionState.ReadyForRequest,
val onCancel: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -81,7 +81,6 @@ internal class StateBuilder(
onMaxAmountSelected = actions.onMaxAmountSelected,
updateInProgress = true,
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet,
providerState = ProviderState.Empty(),
)
}
@ -381,10 +380,13 @@ internal class StateBuilder(
}
fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder {
return if (uiState.permissionState is SwapPermissionState.ReadyForRequest) {
val config = uiState.bottomSheetConfig?.content as? GivePermissionBottomSheetConfig
return if (config != null) {
uiState.copy(
permissionState = uiState.permissionState.copy(
approveType = approveType,
bottomSheetConfig = uiState.bottomSheetConfig.copy(
content = config.copy(
data = config.data.copy(approveType = approveType),
),
),
)
} else {

View file

@ -7,6 +7,7 @@ import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.ApproveType
data class SwapProcessDataState(
// Initial network id
@ -16,6 +17,7 @@ data class SwapProcessDataState(
// Amount from input
val amount: String? = null,
val approveDataModel: RequestApproveStateData? = null,
val approveType: ApproveType? = null,
val swapDataModel: SwapDataModel? = null,
val selectedFee: TxFee? = null, // todo
val tokensDataState: TokensDataStateExpress? = null,

View file

@ -22,10 +22,7 @@ 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.models.SwapPermissionState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.UiActions
import com.tangem.feature.swap.models.toDomainApproveType
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
@ -320,6 +317,7 @@ internal class SwapViewModel @Inject constructor(
dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
dataState.copy(
approveDataModel = permissionState.requestApproveData,
approveType = dataState.approveType ?: ApproveType.UNLIMITED,
)
} else {
dataState.copy(
@ -407,12 +405,15 @@ internal class SwapViewModel @Inject constructor(
fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) {
"dataState.fromCurrency might not be null"
},
approveType = requireNotNull(uiState.permissionState as? SwapPermissionState.ReadyForRequest) {
"uiState.permissionState should be SwapPermissionState.ReadyForRequest"
}.approveType.toDomainApproveType(),
approveType = requireNotNull(dataState.approveType) {
"uiState.permissionState should not be null"
}.toDomainApproveType(),
txFee = requireNotNull(dataState.selectedFee) {
"dataState.selectedFee shouldn't be null"
},
spenderAddress = requireNotNull(dataState.approveDataModel?.spenderAddress) {
"dataState.approveDataModel.spenderAddress shouldn't be null"
},
),
)
}
@ -604,15 +605,16 @@ internal class SwapViewModel @Inject constructor(
openPermissionBottomSheet = {
singleTaskScheduler.cancelTask()
analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked)
uiState = stateBuilder.showPermissionBottomSheet(uiState) { stateBuilder.dismissBottomSheet(uiState) }
},
hidePermissionBottomSheet = {
startLoadingQuotesFromLastState()
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked)
uiState = stateBuilder.showPermissionBottomSheet(uiState) {
startLoadingQuotesFromLastState()
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked)
stateBuilder.dismissBottomSheet(uiState)
}
},
onAmountSelected = { onAmountSelected(it) },
onChangeApproveType = { approveType ->
uiState = stateBuilder.updateApproveType(uiState, approveType)
dataState = dataState.copy(approveType = approveType)
},
onClickFee = {
val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL