Updated on 2026-08-14
This commit is contained in:
parent
3416b28b13
commit
2a5649c941
13 changed files with 101 additions and 62 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -9,4 +9,5 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
|||
*/
|
||||
data class QuoteModel(
|
||||
val toTokenAmount: SwapAmount,
|
||||
val allowanceContract: String?,
|
||||
)
|
||||
|
|
@ -61,6 +61,7 @@ data class RequestApproveStateData(
|
|||
val fee: TxFeeState,
|
||||
val approveData: String,
|
||||
val fromTokenAmount: SwapAmount,
|
||||
val spenderAddress: String,
|
||||
)
|
||||
|
||||
// data class SwapStateData(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ data class SwapStateHolder(
|
|||
val onSuccess: (() -> Unit)? = null,
|
||||
val onMaxAmountSelected: (() -> Unit)? = null,
|
||||
val onShowPermissionBottomSheet: () -> Unit = {},
|
||||
val onCancelPermissionBottomSheet: () -> Unit = {},
|
||||
)
|
||||
|
||||
sealed class SwapCardState {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue