Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 15:15:08 +05:00
parent a3b65cd194
commit d22bfd0c87
27 changed files with 342 additions and 139 deletions

View file

@ -16,6 +16,7 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent {
val spenderAddress: String,
val subtitle: TextReference,
val isHoldToConfirm: Boolean = false,
val isResetApproval: Boolean = false,
val callback: Callback,
)

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
@ -23,7 +24,6 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import com.tangem.common.ui.R as CommonUiR
internal class DefaultGiveApprovalComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@ -38,6 +38,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor(
params = FeeSelectorParams.FeeSelectorBlockParams(
state = FeeSelectorUM.Loading,
onLoadFee = { model.loadFee() },
onDisableCustomFee = { model.shouldDisableCustomFee() },
onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) },
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
@ -71,22 +72,23 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor(
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = config,
containerColor = TangemTheme.colors.background.secondary,
titleText = resourceReference(CommonUiR.string.give_permission_title),
titleText = resourceReference(
if (uiState.isResetApproval) {
R.string.update_approval_permission_title
} else {
R.string.give_permission_title
},
),
titleAction = TopAppBarButtonUM.Icon(
iconRes = CommonUiR.drawable.ic_information_24,
iconRes = R.drawable.ic_information_24,
onClicked = model::showPermissionInfoDialog,
),
) {
GiveApprovalContent(
currency = currency,
subtitle = params.subtitle,
approveType = uiState.approveType,
approveItems = uiState.approveItems,
uiState = uiState,
onChangeApproveType = model::onChangeApproveType,
walletInteractionIcon = uiState.walletInteractionIcon,
isApproveEnabled = uiState.isApproveButtonEnabled,
isApproveLoading = uiState.isApproveLoading,
isHoldToConfirm = uiState.isHoldToConfirm,
onApproveClick = model::onApproveClick,
onCancelClick = model::onCancelClick,
onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick,

View file

@ -5,6 +5,7 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender.MultipleTransactionSendMode
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
@ -22,8 +23,10 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.AllowanceInfo
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
@ -32,6 +35,7 @@ import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -41,15 +45,17 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
@Stable
@ModelScoped
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class GiveApprovalModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
@ -77,11 +83,14 @@ internal class GiveApprovalModel @Inject constructor(
isApproveButtonEnabled = false,
isApproveLoading = false,
isHoldToConfirm = params.isHoldToConfirm,
isResetApproval = params.isResetApproval,
),
)
private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
private var approvalTxList: Map<TransactionData.Uncompiled, TransactionFee> = emptyMap()
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
this.feeSelectorUM = feeSelectorUM
uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) }
@ -122,83 +131,127 @@ internal class GiveApprovalModel @Inject constructor(
)
}
suspend fun prepareApprovalTransaction(): Either<Throwable, TransactionData> {
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
?: return Either.Left(IllegalStateException("Currency is not a token"))
return createApprovalTransactionUseCase(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWalletId = params.userWalletId,
amount = getApprovalAmount(),
contractAddress = tokenCurrency.contractAddress,
spenderAddress = params.spenderAddress,
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
return onApprovalTx(
onApprove = { approve ->
getFeeUseCase(
transactionData = approve,
userWallet = userWallet,
network = params.cryptoCurrencyStatus.currency.network,
).onRight { fee ->
approvalTxList = mapOf(approve to fee)
}
},
onResetApprove = { (revokeApproval, approve) ->
getFeeUseCase(
transactionData = revokeApproval,
userWallet = userWallet,
network = params.cryptoCurrencyStatus.currency.network,
).map { revokeFee ->
estimateFeeForResetApproval(
revokeTransactionFee = revokeFee,
revokeApprovalTransaction = revokeApproval,
approvalTransaction = approve,
)
}
},
)
}
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
val approvalTransaction = prepareApprovalTransaction()
.getOrElse { return GetFeeError.DataError(it).left() }
return getFeeUseCase(
transactionData = approvalTransaction,
userWallet = userWallet,
network = params.cryptoCurrencyStatus.currency.network,
)
fun shouldDisableCustomFee(): Boolean {
return approvalTxList.size > 1
}
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
val approvalTransaction = prepareApprovalTransaction()
.getOrElse { return GetFeeError.DataError(it).left() }
val approve = createApprovalTransactionUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
amount = getApprovalAmount(),
contractAddress = (params.cryptoCurrencyStatus.currency as CryptoCurrency.Token).contractAddress,
spenderAddress = params.spenderAddress,
).getOrElse { error ->
TangemLogger.e("Failed to create approveTransaction", error)
return GetFeeError.DataError(error).left()
}
return if (maybeToken == null) {
getFeeForGaslessUseCase(
transactionData = approvalTransaction,
transactionData = approve,
userWallet = userWallet,
network = params.cryptoCurrencyStatus.currency.network,
)
} else {
getFeeForTokenUseCase(
transactionData = approvalTransaction,
transactionData = approve,
userWallet = userWallet,
token = maybeToken.currency,
)
}
}
private fun estimateFeeForResetApproval(
revokeTransactionFee: TransactionFee,
revokeApprovalTransaction: TransactionData.Uncompiled,
approvalTransaction: TransactionData.Uncompiled,
) = when (revokeTransactionFee) {
is TransactionFee.Choosable -> {
val approveFee = revokeTransactionFee.copy(
minimum = revokeTransactionFee.minimum.increaseEthereumGasLimitBy(2.toBigDecimal()),
normal = revokeTransactionFee.normal.increaseEthereumGasLimitBy(2.toBigDecimal()),
priority = revokeTransactionFee.priority.increaseEthereumGasLimitBy(2.toBigDecimal()),
)
approvalTxList = mapOf(
revokeApprovalTransaction to revokeTransactionFee,
approvalTransaction to approveFee,
)
revokeTransactionFee.copy(
minimum = approveFee.minimum + revokeTransactionFee.minimum,
normal = approveFee.normal + revokeTransactionFee.normal,
priority = approveFee.priority + revokeTransactionFee.priority,
)
}
is TransactionFee.Single -> {
val approveFee = revokeTransactionFee.copy(normal = revokeTransactionFee.normal)
approvalTxList = mapOf(
revokeApprovalTransaction to revokeTransactionFee,
approvalTransaction to approveFee,
)
revokeTransactionFee.copy(normal = approveFee.normal + revokeTransactionFee.normal)
}
}
private suspend fun sendApprovalTransaction(): Boolean {
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false
val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false
val selectedFee = feeContent.selectedFeeItem.fee
val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
val transactionData = createApprovalTransactionUseCase(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWalletId = params.userWalletId,
amount = getApprovalAmount(),
fee = selectedFee,
contractAddress = tokenCurrency.contractAddress,
spenderAddress = params.spenderAddress,
).getOrElse { error ->
TangemLogger.e("Failed to create approval transaction", error)
return false
val transactions = approvalTxList.map { (tx, fee) ->
tx.copy(
fee = when (feeContent.selectedFeeItem) {
is FeeItem.Fast -> (fee as? TransactionFee.Choosable)?.priority ?: fee.normal
is FeeItem.Market -> fee.normal
is FeeItem.Slow -> (fee as? TransactionFee.Choosable)?.minimum ?: fee.normal
else -> feeContent.selectedFeeItem.fee
},
)
}
return if (isFeeInTokenCurrency) {
createAndSendGaslessTransactionUseCase(
userWallet = userWallet,
transactionData = transactionData,
transactionData = transactions.first(),
fee = feeExtended,
)
} else {
sendTransactionUseCase(
txData = transactionData,
txsData = transactions,
userWallet = userWallet,
network = tokenCurrency.network,
sendMode = MultipleTransactionSendMode.DEFAULT,
)
}.fold(
ifLeft = { error ->
@ -241,4 +294,88 @@ internal class GiveApprovalModel @Inject constructor(
null
}
}
private suspend fun <T> onApprovalTx(
onApprove: suspend (TransactionData.Uncompiled) -> Either<GetFeeError, T>,
onResetApprove:
suspend (Pair<TransactionData.Uncompiled, TransactionData.Uncompiled>) -> Either<GetFeeError, T>,
): Either<GetFeeError, T> {
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
?: return GetFeeError.DataError(IllegalStateException("Currency is not a token")).left()
val amount = params.amount.toBigDecimalOrNull()
?: return GetFeeError.DataError(IllegalArgumentException("Invalid amount format")).left()
val allowance = getAllowanceInfoUseCase(
userWalletId = params.userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
spenderAddress = params.spenderAddress,
requiredAmount = amount,
).getOrElse { error ->
TangemLogger.e("Failed to get allowance info", error)
return GetFeeError.DataError(error).left()
}
val approve = createApprovalTransactionUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amount = getApprovalAmount(),
contractAddress = tokenCurrency.contractAddress,
spenderAddress = params.spenderAddress,
).getOrElse { error ->
TangemLogger.e("Failed to create approveTransaction", error)
return GetFeeError.DataError(error).left()
}
return if (allowance is AllowanceInfo.ResetNeeded) {
val revokeApproval = createApprovalTransactionUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amount = BigDecimal.ZERO,
contractAddress = tokenCurrency.contractAddress,
spenderAddress = params.spenderAddress,
).getOrElse { error ->
TangemLogger.e("Failed to create revoke approveTransaction", error)
return GetFeeError.DataError(error).left()
}
onResetApprove(revokeApproval to approve)
} else {
onApprove(approve)
}
}
private fun Fee.increaseEthereumGasLimitBy(multiplier: BigDecimal): Fee {
if (this !is Fee.Ethereum) return this
val increasedGasPrice = amount.value?.movePointRight(amount.decimals)
?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP)
val increasedGasLimit = gasLimit
.multiply(multiplier.toBigInteger())
val increasedAmount = amount.copy(
value = increasedGasPrice?.multiply(
increasedGasLimit.toBigDecimal().movePointLeft(amount.decimals),
),
)
return when (this) {
is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
is Fee.Ethereum.TokenCurrency -> error("handle in [REDACTED_TASK_KEY]")
}
}
private operator fun Fee.plus(otherFee: Fee): Fee {
if (this !is Fee.Ethereum || otherFee !is Fee.Ethereum) return this
val gasLimit = this.gasLimit
val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals)
?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP)
val increasedGasLimit = gasLimit + otherFee.gasLimit
val increasedAmount = this.amount.copy(
value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals),
)
return when (this) {
is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
is Fee.Ethereum.TokenCurrency -> this
}
}
}

View file

@ -12,4 +12,5 @@ internal data class GiveApprovalUM(
val isApproveButtonEnabled: Boolean,
val isApproveLoading: Boolean,
val isHoldToConfirm: Boolean = false,
val isResetApproval: Boolean = false,
)

View file

@ -27,28 +27,24 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.PopupProperties
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.approval.impl.model.GiveApprovalUM
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import com.tangem.common.ui.R as CommonUiR
@Composable
@Suppress("LongParameterList")
internal fun GiveApprovalContent(
currency: String,
uiState: GiveApprovalUM,
subtitle: TextReference,
approveType: ApproveType,
approveItems: ImmutableList<ApproveType>,
onChangeApproveType: (ApproveType) -> Unit,
walletInteractionIcon: Int?,
isApproveEnabled: Boolean,
isApproveLoading: Boolean,
isHoldToConfirm: Boolean,
onApproveClick: () -> Unit,
onCancelClick: () -> Unit,
onOpenLearnMoreAboutApproveClick: () -> Unit,
@ -73,20 +69,21 @@ internal fun GiveApprovalContent(
ApprovalInfo(
currency = currency,
approveType = approveType,
approveItems = approveItems,
approveType = uiState.approveType,
approveItems = uiState.approveItems,
onChangeApproveType = onChangeApproveType,
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
feeSelectorBlockComponent = feeSelectorBlockComponent,
isResetApproval = uiState.isResetApproval,
)
SpacerH(height = TangemTheme.dimens.spacing20)
if (isHoldToConfirm) {
if (uiState.isHoldToConfirm) {
HoldToConfirmButton(
text = stringResourceSafe(id = CommonUiR.string.common_approve),
enabled = isApproveEnabled,
isLoading = isApproveLoading,
text = stringResourceSafe(id = R.string.common_approve),
enabled = uiState.isApproveButtonEnabled,
isLoading = uiState.isApproveLoading,
onConfirm = onApproveClick,
modifier = Modifier
.fillMaxWidth()
@ -94,21 +91,21 @@ internal fun GiveApprovalContent(
)
} else {
PrimaryButtonIconEnd(
text = stringResourceSafe(id = CommonUiR.string.common_approve),
iconResId = walletInteractionIcon,
showProgress = isApproveLoading,
text = stringResourceSafe(id = R.string.common_approve),
iconResId = uiState.walletInteractionIcon,
showProgress = uiState.isApproveLoading,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
onClick = onApproveClick,
enabled = isApproveEnabled,
enabled = uiState.isApproveButtonEnabled,
)
}
SpacerH12()
SecondaryButton(
text = stringResourceSafe(id = CommonUiR.string.common_cancel),
text = stringResourceSafe(id = R.string.common_cancel),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
@ -124,6 +121,7 @@ internal fun GiveApprovalContent(
private fun ApprovalInfo(
currency: String,
approveType: ApproveType,
isResetApproval: Boolean,
approveItems: ImmutableList<ApproveType>,
onChangeApproveType: (ApproveType) -> Unit,
onOpenLearnMoreAboutApproveClick: () -> Unit,
@ -131,7 +129,7 @@ private fun ApprovalInfo(
) {
FooterContainer(
footer = annotatedReference {
append(stringResourceSafe(CommonUiR.string.swap_approve_description))
append(stringResourceSafe(R.string.swap_approve_description))
append(" ")
withLink(
link = LinkAnnotation.Clickable(
@ -140,7 +138,7 @@ private fun ApprovalInfo(
),
block = {
appendColored(
text = stringResourceSafe(CommonUiR.string.common_learn_more),
text = stringResourceSafe(R.string.common_learn_more),
color = TangemTheme.colors.text.accent,
)
},
@ -157,7 +155,13 @@ private fun ApprovalInfo(
}
SpacerH16()
FooterContainer(
footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer),
footer = resourceReference(
if (isResetApproval) {
R.string.update_approval_permission_fee_note
} else {
R.string.give_permission_policy_type_footer
},
),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
feeSelectorBlockComponent.Content(
@ -202,7 +206,7 @@ private fun AmountItem(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency),
text = stringResourceSafe(id = R.string.give_permission_rows_amount, currency),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
@ -215,7 +219,7 @@ private fun AmountItem(
maxLines = 1,
)
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)),
painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
@ -275,10 +279,10 @@ private fun DropdownSelector(
Text(
text = when (item) {
ApproveType.LIMITED -> stringResourceSafe(
id = CommonUiR.string.give_permission_current_transaction,
id = R.string.give_permission_current_transaction,
)
ApproveType.UNLIMITED -> stringResourceSafe(
id = CommonUiR.string.give_permission_unlimited,
id = R.string.give_permission_unlimited,
)
},
color = TangemTheme.colors.text.primary1,
@ -288,7 +292,7 @@ private fun DropdownSelector(
SpacerWMax()
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24),
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
),
tint = color,
contentDescription = null,
@ -316,13 +320,8 @@ private fun GiveApprovalContentPreview(
GiveApprovalContent(
currency = params.currency,
subtitle = params.subtitle,
approveType = params.approveType,
approveItems = params.approveItems,
uiState = params.uiState,
onChangeApproveType = {},
walletInteractionIcon = params.walletInteractionIcon,
isApproveEnabled = params.isApproveEnabled,
isApproveLoading = params.isApproveLoading,
isHoldToConfirm = false,
onApproveClick = {},
onCancelClick = {},
onOpenLearnMoreAboutApproveClick = {},
@ -334,11 +333,7 @@ private fun GiveApprovalContentPreview(
private data class GiveApprovalPreviewParams(
val currency: String,
val subtitle: TextReference,
val approveType: ApproveType,
val approveItems: ImmutableList<ApproveType>,
val walletInteractionIcon: Int?,
val isApproveEnabled: Boolean,
val isApproveLoading: Boolean,
val uiState: GiveApprovalUM,
)
private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<GiveApprovalPreviewParams> {
@ -347,20 +342,36 @@ private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<Give
GiveApprovalPreviewParams(
currency = "USDT",
subtitle = stringReference("Allow this app to access your USDT"),
approveType = ApproveType.LIMITED,
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
isApproveEnabled = true,
isApproveLoading = false,
uiState = GiveApprovalUM(
approveType = ApproveType.LIMITED,
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
walletInteractionIcon = R.drawable.ic_tangem_24,
isApproveButtonEnabled = true,
isApproveLoading = false,
),
),
GiveApprovalPreviewParams(
currency = "USDC",
subtitle = stringReference("Allow this app to access your USDC"),
approveType = ApproveType.UNLIMITED,
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
isApproveEnabled = false,
isApproveLoading = true,
uiState = GiveApprovalUM(
approveType = ApproveType.UNLIMITED,
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
walletInteractionIcon = R.drawable.ic_tangem_24,
isApproveButtonEnabled = false,
isApproveLoading = true,
),
),
GiveApprovalPreviewParams(
currency = "USDC",
subtitle = stringReference("Allow this app to access your USDC"),
uiState = GiveApprovalUM(
approveType = ApproveType.UNLIMITED,
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
walletInteractionIcon = R.drawable.ic_tangem_24,
isApproveButtonEnabled = true,
isApproveLoading = false,
isResetApproval = true,
),
),
)
}

View file

@ -17,6 +17,7 @@ sealed class FeeSelectorParams {
abstract val userWalletId: UserWalletId
abstract val onLoadFeeExtended: (suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended>)?
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
abstract val onDisableCustomFee: () -> Boolean
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus
abstract val feeStateConfiguration: FeeStateConfiguration
@ -39,6 +40,7 @@ sealed class FeeSelectorParams {
override val analyticsCategoryName: String,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
override val shouldShowOnlySpeedOption: Boolean = false,
override val onDisableCustomFee: () -> Boolean = { false },
val bottomSheetShown: (Boolean) -> Unit = {},
) : FeeSelectorParams()
@ -56,6 +58,7 @@ sealed class FeeSelectorParams {
override val analyticsCategoryName: String,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
override val shouldShowOnlySpeedOption: Boolean = false,
override val onDisableCustomFee: () -> Boolean = { false },
val callback: FeeSelectorModelCallback,
) : FeeSelectorParams()

View file

@ -45,6 +45,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
params = FeeSelectorParams.FeeSelectorDetailsParams(
state = model.uiState.value,
onLoadFee = params.onLoadFee,
onDisableCustomFee = params.onDisableCustomFee,
onLoadFeeExtended = params.onLoadFeeExtended,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,

View file

@ -110,6 +110,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
feeStateConfiguration = params.feeStateConfiguration,
isFeeApproximate = isFeeApproximate(fee.transactionFee.normal.amount.type),
feeSelectorIntents = this@FeeSelectorLogic,
shouldDisableCustomFee = params.onDisableCustomFee(),
),
)
},

View file

@ -17,6 +17,7 @@ internal class FeeItemConverter(
private val feeSelectorIntents: FeeSelectorIntents,
private val appCurrency: AppCurrency,
cryptoCurrencyStatus: CryptoCurrencyStatus,
private val shouldDisableCustomFee: Boolean,
) : Converter<FeeItemConverter.Input, ImmutableList<FeeItem>> {
private val customFeeFieldConverter = FeeSelectorCustomFieldConverter(
@ -54,8 +55,10 @@ internal class FeeItemConverter(
add(FeeItem.Market(fee = value.transactionFee.normal))
}
}
val customFee = value.customFee ?: constructCustomFee()
customFee?.let(::add)
if (!shouldDisableCustomFee) {
val customFee = value.customFee ?: constructCustomFee()
customFee?.let(::add)
}
}
private fun MutableList<FeeItem>.addFeeItemsLimited(value: Input) {

View file

@ -22,6 +22,7 @@ internal class FeeSelectorLoadedTransformer(
private val feeStateConfiguration: FeeSelectorParams.FeeStateConfiguration,
private val isFeeApproximate: Boolean,
private val feeSelectorIntents: FeeSelectorIntents,
private val shouldDisableCustomFee: Boolean,
) : Transformer<FeeSelectorUM> {
private val feeItemsConverter = FeeItemConverter(
@ -30,6 +31,7 @@ internal class FeeSelectorLoadedTransformer(
feeSelectorIntents = feeSelectorIntents,
appCurrency = appCurrency,
cryptoCurrencyStatus = feeCryptoCurrencyStatus,
shouldDisableCustomFee = shouldDisableCustomFee,
)
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {

View file

@ -78,6 +78,7 @@ internal class ShowApprovalBottomSheetTransformer(
footerText = resourceReference(R.string.staking_give_permission_fee_footer),
onChangeApproveType = prevState.clickIntents::onApproveTypeChange,
onOpenLearnMoreAboutApproveClick = {},
isResetApproval = false,
),
walletInteractionIcon = walletInterationIcon(userWallet),
onCancel = onDismiss,

View file

@ -10,7 +10,6 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData
@ -2025,35 +2024,31 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
}
// setting up amount for approve with given amount for swap [SwapApproveType.Limited]
val callData = SmartContractCallDataProviderFactory.getApprovalCallData(
spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" },
amount = swapAmount.value.convertToSdkAmount(fromTokenStatus),
blockchain = fromToken.network.toBlockchain(),
)
val feeData = try {
val extras = createTransactionExtrasUseCase(
callData = callData,
network = fromToken.network,
).getOrNull() ?: error("unable to create extras")
val fromAddress = requireNotNull(
fromTokenStatus.value.networkAddress?.defaultAddress?.value,
) { "networkAddress cant be null" }
val fromAddress = requireNotNull(
fromTokenStatus.value.networkAddress?.defaultAddress?.value,
) { "networkAddress cant be null" }
val transactionData = TransactionData.Uncompiled(
amount = createNativeAmountForDex("0", fromToken.network),
destinationAddress = fromToken.getContractAddress(),
fee = null,
sourceAddress = fromAddress,
extras = extras,
)
getFeeUseCase(
transactionData = transactionData,
network = fromToken.network,
userWallet = userWallet,
).getOrNull() ?: error("unable to calculate fee")
} catch (e: Exception) {
TangemLogger.e("Failed to get fee", e)
// it's impossible next steps without fee
val allowanceInfo = getAllowanceInfoUseCase(
userWalletId = userWalletId,
cryptoCurrency = fromToken,
spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" },
requiredAmount = swapAmount.value,
).getOrNull()
val amount = if (allowanceInfo is AllowanceInfo.ResetNeeded) {
BigDecimal.ZERO
} else {
swapAmount.value
}
val approveTransaction = createApprovalTransactionUseCase(
cryptoCurrencyStatus = fromTokenStatus,
userWalletId = userWalletId,
amount = amount,
contractAddress = fromToken.getContractAddress(),
spenderAddress = spenderAddress,
).getOrElse { error ->
TangemLogger.e("Failed to create approveTransaction", error)
return createSwapErrorWith(
fromToken = fromTokenStatus,
fromAccount = fromAccount,
@ -2063,6 +2058,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
}
val feeData = getFeeUseCase(
transactionData = approveTransaction,
network = fromToken.network,
userWallet = userWallet,
).getOrNull() ?: error("unable to calculate fee")
val feeState = feeData
.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX)
.toTxFeeState(fromToken, null)
@ -2084,6 +2085,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
amount = INFINITY_SYMBOL,
walletAddress = getWalletAddress(fromToken.network),
spenderAddress = getTokenAddress(fromToken),
isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded,
requestApproveData = RequestApproveStateData(
fee = feeState,
fromTokenAmount = swapAmount,

View file

@ -87,6 +87,7 @@ sealed class PermissionDataState {
val walletAddress: String,
val spenderAddress: String,
val requestApproveData: RequestApproveStateData,
val isResetApproval: Boolean,
) : PermissionDataState()
object PermissionFailed : PermissionDataState()

View file

@ -206,10 +206,15 @@ internal class DefaultSwapComponent @AssistedInject constructor(
feeCryptoCurrencyStatus = feeCryptoCurrency,
amount = model.dataState.amount.orEmpty(),
spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress,
subtitle = resourceReference(
id = R.string.give_permission_swap_subtitle,
formatArgs = wrappedList(providerName, permissionState.currency),
),
subtitle = if (permissionState.isResetApproval) {
resourceReference(R.string.update_approval_permission_subtitle)
} else {
resourceReference(
id = R.string.give_permission_swap_subtitle,
formatArgs = wrappedList(providerName, permissionState.currency),
)
},
isResetApproval = permissionState.isResetApproval,
isHoldToConfirm = model.isHoldToConfirmEnabled,
callback = model.approvalCallback,
)

View file

@ -975,6 +975,7 @@ internal class StateBuilder(
dialogText = resourceReference(R.string.swapping_approve_information_text),
footerText = resourceReference(R.string.swap_give_permission_fee_footer),
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
isResetApproval = permissionDataState.isResetApproval,
)
}
}