diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt
new file mode 100644
index 0000000000..7a6362efe3
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt
@@ -0,0 +1,51 @@
+package com.tangem.datasource.di
+
+import okhttp3.Interceptor
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+import timber.log.Timber
+
+class StatusCodeInterceptor : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalResponse = chain.proceed(chain.request())
+
+ if (shouldInterceptResponse(originalResponse)) {
+ Timber.e("StatusCodeInterceptor INTERCEPTED%s", originalResponse.request.url.toString())
+
+ val body = getBody().toResponseBody("application/json".toMediaTypeOrNull())
+ val code = getCode()
+
+ return originalResponse.newBuilder()
+ .code(code)
+ .body(body)
+ .build()
+ }
+
+ return originalResponse
+ }
+
+ private fun shouldInterceptResponse(response: Response): Boolean {
+ return response.request.url.toString().contains("exchange-quote")
+ // && response.request.url.toString().contains("changenow")
+ }
+
+ private fun getCode(): Int {
+ return CODE_400
+ }
+
+ private fun getBody(): String {
+ return "\"error\": {\n" +
+ " \"code\": 2290,\n" +
+ " \"description\": \"Core: receivedDecimals is not equal to expressDecimals\",\n" +
+ " \"message\": \"Not valid\",\n" +
+ " \"receivedToDecimals\": 5,\n" +
+ " \"expressToDecimals\": 5\n" +
+ " }"
+ }
+
+ companion object {
+ private const val CODE_400 = 400
+ }
+}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
index 26d5b3cf08..02432a3d03 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
@@ -35,6 +35,8 @@ object PreferencesKeys {
val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") }
+ val SWAP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "swapTransactionsStatuses") }
+
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index 744452a5fa..42f9f8b591 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -194,6 +194,7 @@
Мои токены
У вас нет добавленных токенов. Добавьте токены для обмена
Недоступен для обмена с %s
+ Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес
Статус
Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами
Выберите провайдера
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 55f40ad98b..1150c0349b 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -193,6 +193,7 @@
My tokens
You haven\'t added any tokens yet. Add tokens via Market to swap
Cannot be swapped for %s
+ Additionally, the network fee for sending the exchanged funds back to your address is included in the rate
Status
Providers facilitate transactions, ensuring smooth and efficient token swaps
Choose provider
diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt
index 2998ea46b9..9fe2ba124a 100644
--- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt
+++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt
@@ -88,7 +88,7 @@ class GetCurrencyWarningsUseCase(
showSwapPromoTokenUseCase().conflate(),
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(),
) { shouldShowSwapPromo, isExchangeable ->
- if (shouldShowSwapPromo && isExchangeable) {
+ if (shouldShowSwapPromo && isExchangeable && currencyStatus.value !is CryptoCurrencyStatus.Unreachable) {
cryptoStatuses.fold(
ifLeft = { null },
ifRight = { cryptoCurrencyStatuses ->
diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt
index 97b9a1e2b2..f617305f17 100644
--- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt
+++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt
@@ -4,9 +4,11 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
+import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.SwapTransactionRepository
+import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
import com.tangem.feature.swap.domain.models.domain.SavedLastSwappedCryptoCurrency
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
@@ -24,11 +26,11 @@ class DefaultSwapTransactionRepository(
toCryptoCurrencyId: CryptoCurrency.ID,
transaction: SavedSwapTransactionModel,
) {
+ transaction.status?.let { storeTransactionState(transaction.txId, it) }
appPreferencesStore.editData { mutablePreferences ->
val savedTransactions: List? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
)
-
val tokenTransactions = savedTransactions
?.firstOrNull {
it.checkId(
@@ -62,14 +64,17 @@ class DefaultSwapTransactionRepository(
}
}
- override fun getTransactions(
+ override suspend fun getTransactions(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow?> {
+ val txStatuses = appPreferencesStore.getObjectMap(
+ key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
+ )
return appPreferencesStore.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
).map { savedTransactions ->
- savedTransactions
+ val currencyTxs = savedTransactions
?.filter {
it.userWalletId == userWalletId.stringValue &&
(
@@ -77,6 +82,14 @@ class DefaultSwapTransactionRepository(
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
+
+ currencyTxs?.map { currencyTx ->
+ currencyTx.copy(
+ transactions = currencyTx.transactions.map { tx ->
+ tx.copy(status = txStatuses[tx.txId])
+ },
+ )
+ }
}
}
@@ -86,6 +99,7 @@ class DefaultSwapTransactionRepository(
toCryptoCurrencyId: CryptoCurrency.ID,
txId: String,
) {
+ clearTransactionsStatuses(txId = txId)
appPreferencesStore.editData { mutablePreferences ->
val savedList: List? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
@@ -130,6 +144,22 @@ class DefaultSwapTransactionRepository(
}
}
+ override suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel) {
+ appPreferencesStore.editData { mutablePreferences ->
+ val savedMap = mutablePreferences.getObjectMap(
+ key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
+ )
+
+ val updatesMap = savedMap?.toMutableMap() ?: mutableMapOf()
+ updatesMap[txId] = status
+
+ mutablePreferences.setObjectMap(
+ key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
+ value = updatesMap,
+ )
+ }
+ }
+
override suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? {
val lastSwappedCurrencies = appPreferencesStore.getObjectListSync(
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
@@ -194,4 +224,22 @@ class DefaultSwapTransactionRepository(
},
)
}
+
+ private suspend fun clearTransactionsStatuses(txId: String) {
+ appPreferencesStore.editData { mutablePreferences ->
+ val savedList = mutablePreferences.getObjectMap(
+ key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
+ )
+ val editedList = savedList?.filterNot { it.key == txId }
+
+ if (editedList.isNullOrEmpty()) {
+ mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY)
+ } else {
+ mutablePreferences.setObjectMap(
+ key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
+ value = editedList,
+ )
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt
index 2068fc982a..6ca55d7f04 100644
--- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt
+++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt
@@ -39,7 +39,7 @@ internal class ErrorsDataConverter(
receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals),
expressFromDecimals = requireNotNull(error.value?.expressFromDecimals),
)
- else -> DataError.UnknownError
+ else -> DataError.UnknownErrorWithCode(error.code)
}
} catch (e: Exception) {
return DataError.UnknownError
diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt
index 2c3a3e6af6..829cb5e7e4 100644
--- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt
+++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt
@@ -30,6 +30,8 @@ sealed class DataError {
val expressFromDecimals: Int,
) : DataError()
+ data class UnknownErrorWithCode(override val code: Int) : DataError()
+
object UnknownError : DataError() {
override val code: Int = -1
}
diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt
index 97c7c85fb3..6711df346c 100644
--- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt
+++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt
@@ -37,6 +37,7 @@ sealed interface SwapState {
data class SwapError(
val fromTokenInfo: TokenSwapInfo,
val error: DataError,
+ val includeFeeInAmount: IncludeFeeInAmount,
) : SwapState
}
diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt
index 526b5b2a73..132327e4cb 100644
--- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt
+++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt
@@ -11,6 +11,7 @@ sealed class TxState {
val toAmountValue: BigDecimal? = null,
val txAddress: String,
val txExternalUrl: String? = null,
+ val txUrl: String? = null,
val timestamp: Long,
) : TxState()
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
index 2fd8561359..34c9756d26 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
@@ -9,7 +9,6 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
-import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.utils.convertToAmount
@@ -37,6 +36,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.firstOrNull
import timber.log.Timber
import java.math.BigDecimal
+import java.math.BigInteger
import java.math.RoundingMode
import javax.inject.Inject
@@ -54,6 +54,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val dispatcher: CoroutineDispatcherProvider,
private val swapTransactionRepository: SwapTransactionRepository,
private val initialToCurrencyResolver: InitialToCurrencyResolver,
+ private val blockchainInteractor: BlockchainInteractor,
) : SwapInteractor {
private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) {
@@ -62,7 +63,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val swapCurrencyConverter = SwapCurrencyConverter()
private val amountFormatter = AmountFormatter()
- private var network: Network? = null
+ private val hundredPercent = BigInteger("100")
override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress {
val selectedWallet = getSelectedWalletSyncUseCase().fold(
@@ -263,7 +264,6 @@ internal class SwapInteractorImpl @Inject constructor(
provider = provider,
amount = amount,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
- selectedFee = selectedFee,
)
}
}
@@ -340,7 +340,6 @@ internal class SwapInteractorImpl @Inject constructor(
provider: SwapProvider,
amount: SwapAmount,
isBalanceWithoutFeeEnough: Boolean,
- selectedFee: FeeType,
): Pair {
return provider to loadCexQuoteData(
exchangeProviderType = ExchangeProviderType.CEX,
@@ -351,7 +350,6 @@ internal class SwapInteractorImpl @Inject constructor(
isAllowedToSpend = true,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
provider = provider,
- selectedFee = selectedFee,
)
}
@@ -411,7 +409,6 @@ internal class SwapInteractorImpl @Inject constructor(
txFee = state.txFee,
amount = amount,
fromToken = fromToken.currency,
- selectedFee = selectedFee,
)
return state.copy(
permissionState = PermissionDataState.Empty,
@@ -539,6 +536,10 @@ internal class SwapInteractorImpl @Inject constructor(
},
ifRight = {
val timestamp = System.currentTimeMillis()
+ val txUrl = blockchainInteractor.getExplorerTransactionLink(
+ networkId = currencyToSend.currency.network.backendId,
+ txAddress = exchangeData.transaction.txTo,
+ )
storeSwapTransaction(
currencyToSend = currencyToSend,
currencyToGet = currencyToGet,
@@ -546,6 +547,7 @@ internal class SwapInteractorImpl @Inject constructor(
swapProvider = swapProvider,
swapDataModel = exchangeData,
timestamp = timestamp,
+ txUrl = txUrl,
)
storeLastCryptoCurrencyId(currencyToGet.currency)
TxState.TxSent(
@@ -564,6 +566,7 @@ internal class SwapInteractorImpl @Inject constructor(
derivationPath,
).orEmpty(),
txExternalUrl = externalUrl,
+ txUrl = txUrl,
timestamp = timestamp,
)
},
@@ -580,10 +583,11 @@ internal class SwapInteractorImpl @Inject constructor(
)
return if (fee.gasLimit != 0) {
+ val feeAmountWithDecimals = feeAmountValue.movePointRight(fee.decimals)
Fee.Ethereum(
amount = feeAmount,
gasLimit = fee.gasLimit.toBigInteger(),
- gasPrice = (feeAmountValue / fee.gasLimit.toBigDecimal()).toBigInteger(),
+ gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(),
)
} else {
Fee.Common(feeAmount)
@@ -597,6 +601,7 @@ internal class SwapInteractorImpl @Inject constructor(
swapProvider: SwapProvider,
swapDataModel: SwapDataModel,
timestamp: Long,
+ txUrl: String,
) {
swapTransactionRepository.storeTransaction(
userWalletId = UserWalletId(userWalletManager.getWalletId()),
@@ -608,6 +613,12 @@ internal class SwapInteractorImpl @Inject constructor(
timestamp = timestamp,
fromCryptoAmount = amount.value,
toCryptoAmount = swapDataModel.toTokenAmount.value,
+ status = ExchangeStatusModel(
+ providerId = swapProvider.providerId,
+ status = ExchangeStatus.New,
+ txId = swapDataModel.transaction.txId,
+ txUrl = txUrl,
+ ),
),
)
}
@@ -709,7 +720,6 @@ internal class SwapInteractorImpl @Inject constructor(
provider: SwapProvider,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
- selectedFee: FeeType,
): SwapState {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
@@ -725,7 +735,6 @@ internal class SwapInteractorImpl @Inject constructor(
txFee = txFee,
amount = amount,
fromToken = fromToken,
- selectedFee = selectedFee,
)
val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) {
includeFeeInAmount.amountSubtractFee
@@ -824,7 +833,7 @@ internal class SwapInteractorImpl @Inject constructor(
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
)
- return SwapState.SwapError(fromTokenSwapInfo, error)
+ return SwapState.SwapError(fromTokenSwapInfo, error, includeFeeInAmount)
},
)
}
@@ -834,7 +843,6 @@ internal class SwapInteractorImpl @Inject constructor(
txFee: TxFeeState,
amount: SwapAmount,
fromToken: CryptoCurrency,
- selectedFee: FeeType,
): IncludeFeeInAmount {
if (fromToken is CryptoCurrency.Token) {
return IncludeFeeInAmount.Excluded
@@ -851,11 +859,7 @@ internal class SwapInteractorImpl @Inject constructor(
}
val feeValue = when (txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
- is TxFeeState.MultipleFeeState -> if (selectedFee == FeeType.NORMAL) {
- txFee.normalFee.feeValue
- } else {
- txFee.priorityFee.feeValue
- }
+ is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
}
@@ -965,6 +969,7 @@ internal class SwapInteractorImpl @Inject constructor(
SwapState.SwapError(
fromTokenSwapInfo,
error,
+ IncludeFeeInAmount.Excluded,
)
},
)
@@ -1191,8 +1196,10 @@ internal class SwapInteractorImpl @Inject constructor(
val decimals = transactionManager.getNativeTokenDecimals(networkId)
return when (this) {
is TransactionFee.Choosable -> {
- val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO
- val feePriority = this.priority.amount.value ?: BigDecimal.ZERO
+ val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND)
+ val priorityFee = this.priority.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND)
+ val feeNormal = normalFee.amount.value ?: BigDecimal.ZERO
+ val feePriority = priorityFee.amount.value ?: BigDecimal.ZERO
val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0]
val priorityFiatValue = getFormattedFiatFees(networkId, feePriority)[0]
@@ -1207,7 +1214,7 @@ internal class SwapInteractorImpl @Inject constructor(
TxFeeState.MultipleFeeState(
normalFee = TxFee(
feeValue = feeNormal,
- gasLimit = this.normal.getGasLimit(),
+ gasLimit = normalFee.getGasLimit(),
feeFiatFormatted = normalFiatValue,
feeCryptoFormatted = normalCryptoFee,
decimals = decimals,
@@ -1216,7 +1223,7 @@ internal class SwapInteractorImpl @Inject constructor(
),
priorityFee = TxFee(
feeValue = feePriority,
- gasLimit = this.priority.getGasLimit(),
+ gasLimit = priorityFee.getGasLimit(),
feeFiatFormatted = priorityFiatValue,
feeCryptoFormatted = priorityCryptoFee,
decimals = decimals,
@@ -1247,6 +1254,26 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
+ /**
+ * Workaround to increase gas limit cause we calculate fee for random address
+ */
+ private fun Fee.increaseGasLimitBy(percentage: Int): Fee {
+ if (this !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
+ .multiply(percentage.toBigInteger())
+ .divide(hundredPercent)
+ val increasedAmount = this.amount.copy(
+ value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals),
+ )
+ return this.copy(
+ amount = increasedAmount,
+ gasLimit = increasedGasLimit,
+ )
+ }
+
private fun hasOutgoingTransaction(cryptoCurrencyStatuses: CryptoCurrencyStatus): Boolean {
return cryptoCurrencyStatuses.value.pendingTransactions.any { it.isOutgoing }
}
@@ -1377,6 +1404,7 @@ internal class SwapInteractorImpl @Inject constructor(
companion object {
@Suppress("UnusedPrivateMember")
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
+ private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
private const val INFINITY_SYMBOL = "∞"
private val ONE_INCH_SUPPORTED_NETWORKS = listOf(
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt
index 2db58ee01d..e00dadb2a9 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt
@@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
+import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
import kotlinx.coroutines.flow.Flow
@@ -15,7 +16,7 @@ interface SwapTransactionRepository {
transaction: SavedSwapTransactionModel,
)
- fun getTransactions(
+ suspend fun getTransactions(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow?>
@@ -27,6 +28,8 @@ interface SwapTransactionRepository {
txId: String,
)
+ suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel)
+
suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID)
suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String?
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
index d299f7824e..518d7c9fc8 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt
@@ -42,6 +42,7 @@ class SwapDomainModule {
walletManagersFacade: WalletManagersFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
initialToCurrencyResolver: InitialToCurrencyResolver,
+ blockchainInteractor: BlockchainInteractor,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@@ -56,6 +57,7 @@ class SwapDomainModule {
dispatcher = coroutineDispatcherProvider,
swapTransactionRepository = swapTransactionRepository,
initialToCurrencyResolver = initialToCurrencyResolver,
+ blockchainInteractor = blockchainInteractor,
)
}
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt
index e01a8b8058..6ca10e195f 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt
@@ -11,6 +11,7 @@ sealed class FeeItemState {
val amountCrypto: String,
val symbolCrypto: String,
val amountFiatFormatted: String,
+ val explanation: TextReference?,
val isClickable: Boolean,
val onClick: () -> Unit,
) : FeeItemState()
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt
index d0f47a62a2..3c3de0d27f 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt
@@ -109,6 +109,7 @@ private fun ChooseFeeBottomSheetContent_Preview() {
amountCrypto = "1000",
symbolCrypto = "MATIC",
amountFiatFormatted = "(10$)",
+ explanation = null,
isClickable = false,
onClick = {},
),
@@ -118,6 +119,7 @@ private fun ChooseFeeBottomSheetContent_Preview() {
amountCrypto = "2000",
symbolCrypto = "MATIC",
amountFiatFormatted = "(10$)",
+ explanation = null,
isClickable = false,
onClick = {},
),
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt
index f379ae3511..f5385bf456 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt
@@ -3,6 +3,8 @@ package com.tangem.feature.swap.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
+import androidx.compose.material.Text
+import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -24,12 +26,13 @@ fun FeeItemBlock(state: FeeItemState) {
@Composable
fun FeeItem(state: FeeItemState.Content) {
- Box(
+ Column(
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
+ .padding(start = TangemTheme.dimens.spacing12)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.clickable(
onClick = state.onClick,
@@ -40,13 +43,32 @@ fun FeeItem(state: FeeItemState.Content) {
val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiatFormatted})"
SimpleActionRow(
modifier = Modifier.padding(
- start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
),
title = state.title.resolveReference(),
description = description,
isClickable = state.isClickable,
)
+ state.explanation?.let {
+ Divider(
+ color = TangemTheme.colors.stroke.primary,
+ thickness = TangemTheme.dimens.size0_5,
+ modifier = Modifier.padding(
+ top = TangemTheme.dimens.spacing10,
+ bottom = TangemTheme.dimens.spacing10,
+ end = TangemTheme.dimens.spacing2,
+ ),
+ )
+ Text(
+ text = it.resolveReference(),
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ modifier = Modifier.padding(
+ bottom = TangemTheme.dimens.spacing10,
+ end = TangemTheme.dimens.spacing16,
+ ),
+ )
+ }
}
}
@@ -59,6 +81,10 @@ private fun FeeItemPreview() {
amountCrypto = "1000",
symbolCrypto = "MATIC",
amountFiatFormatted = "(1000$)",
+ explanation = stringReference(
+ "Additionally, the network fee for sending the exchanged funds back to your address is " +
+ "included in the rate",
+ ),
isClickable = false,
onClick = {},
)
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
index c74bd436f7..cbc20fe451 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
@@ -215,7 +215,7 @@ internal class StateBuilder(
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val warnings = getWarningsForSuccessState(quoteModel, fromToken)
- val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
+ val feeState = createFeeState(quoteModel.txFee, selectedFeeType, swapProvider)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
return uiStateHolder.copy(
@@ -378,6 +378,7 @@ internal class StateBuilder(
}
private fun getSwapButtonEnabled(preparedSwapConfigState: PreparedSwapConfigState): Boolean {
+ if (preparedSwapConfigState.hasOutgoingTransaction) return false
return when (preparedSwapConfigState.includeFeeInAmount) {
IncludeFeeInAmount.BalanceNotEnough -> false
IncludeFeeInAmount.Excluded ->
@@ -394,12 +395,21 @@ internal class StateBuilder(
swapProvider: SwapProvider,
fromToken: TokenSwapInfo,
toToken: CryptoCurrencyStatus?,
+ includeFeeInAmount: IncludeFeeInAmount,
dataError: DataError,
isReverseSwapPossible: Boolean,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
- val warning = getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency)
+ val warnings = mutableListOf()
+ warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency))
+ if (includeFeeInAmount is IncludeFeeInAmount.Included) {
+ warnings.add(
+ SwapWarning.GeneralWarning(
+ createNetworkFeeCoverageNotificationConfig(),
+ ),
+ )
+ }
val providerState = getProviderStateForError(
swapProvider = swapProvider,
fromToken = fromToken.cryptoCurrencyStatus.currency,
@@ -437,7 +447,7 @@ internal class StateBuilder(
amountEquivalent = getFormattedFiatAmount(fromToken.amountFiat),
),
receiveCardData = receiveCardData,
- warnings = listOf(warning),
+ warnings = warnings,
permissionState = SwapPermissionState.Empty,
fee = FeeItemState.Empty,
swapButton = SwapButton(
@@ -666,7 +676,7 @@ internal class StateBuilder(
)
}
- private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType): FeeItemState {
+ private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType, swapProvider: SwapProvider): FeeItemState {
val isClickable: Boolean
val fee = when (txFeeState) {
TxFeeState.Empty -> return FeeItemState.Empty
@@ -692,6 +702,11 @@ internal class StateBuilder(
title = resourceReference(R.string.common_fee_label),
amountCrypto = fee.feeCryptoFormatted,
symbolCrypto = fee.cryptoSymbol,
+ explanation = if (swapProvider.type == ExchangeProviderType.CEX) {
+ resourceReference(R.string.express_cex_fee_explanation)
+ } else {
+ null
+ },
amountFiatFormatted = fee.feeFiatFormatted,
isClickable = isClickable,
onClick = actions.onClickFee,
@@ -717,7 +732,6 @@ internal class StateBuilder(
fun createSuccessState(
uiState: SwapStateHolder,
txState: TxState.TxSent,
- txUrl: String,
dataState: SwapProcessDataState,
onExploreClick: () -> Unit,
onStatusClick: () -> Unit,
@@ -735,7 +749,7 @@ internal class StateBuilder(
return uiState.copy(
successState = SwapSuccessStateHolder(
timestamp = txState.timestamp,
- txUrl = txUrl,
+ txUrl = txState.txUrl.orEmpty(),
providerName = stringReference(providerState.name),
providerType = stringReference(providerState.type),
showStatusButton = providerState.type == ExchangeProviderType.CEX.name,
@@ -1002,6 +1016,7 @@ internal class StateBuilder(
amountCrypto = this.normalFee.feeCryptoFormatted,
symbolCrypto = this.normalFee.cryptoSymbol,
amountFiatFormatted = this.normalFee.feeFiatFormatted,
+ explanation = null,
isClickable = true,
onClick = {},
),
@@ -1011,6 +1026,7 @@ internal class StateBuilder(
amountCrypto = this.priorityFee.feeCryptoFormatted,
symbolCrypto = this.priorityFee.cryptoSymbol,
amountFiatFormatted = this.priorityFee.feeFiatFormatted,
+ explanation = null,
isClickable = true,
onClick = {},
),
@@ -1221,7 +1237,7 @@ internal class StateBuilder(
}
private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String {
- return "${this.formatToUIRepresentation()} ${token.network.currencySymbol}"
+ return "${this.formatToUIRepresentation()} ${token.symbol}"
}
private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal {
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
index 54a29db438..1e0b3f5944 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
@@ -467,13 +467,14 @@ private val state = SwapStateHolder(
amountCrypto = "100",
symbolCrypto = "1000",
amountFiatFormatted = "(100)",
+ explanation = null,
isClickable = true,
onClick = {},
),
warnings = listOf(
SwapWarning.PermissionNeeded(
notificationConfig = NotificationConfig(
- title = stringReference("Give Premission"),
+ title = stringReference("Give Permission"),
subtitle = stringReference("To continue swapping you need to give permission to Tangem"),
iconResId = R.drawable.ic_locked_24,
),
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
index fe3d608266..a511e6ab43 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
@@ -344,6 +344,7 @@ internal class SwapViewModel @Inject constructor(
fromToken = state.fromTokenInfo,
toToken = dataState.toCryptoCurrency,
dataError = state.error,
+ includeFeeInAmount = state.includeFeeInAmount,
isReverseSwapPossible = isReverseSwapPossible(),
)
sendErrorAnalyticsEvent(state.error, provider)
@@ -447,19 +448,14 @@ internal class SwapViewModel @Inject constructor(
}.onSuccess {
when (it) {
is TxState.TxSent -> {
- val url = blockchainInteractor.getExplorerTransactionLink(
- networkId = fromCurrency.currency.network.backendId,
- txAddress = it.txAddress,
- )
uiState = stateBuilder.createSuccessState(
uiState = uiState,
txState = it,
dataState = dataState,
- txUrl = url,
onExploreClick = {
- val txHash = it.txAddress
- if (txHash.isNotEmpty()) {
- swapRouter.openUrl(url)
+ val txUrl = it.txUrl
+ if (!txUrl.isNullOrBlank()) {
+ swapRouter.openUrl(txUrl)
}
analyticsEventHandler.send(
event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol),
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt
index 7ef7622a82..4c78d11155 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt
@@ -54,7 +54,7 @@ internal class ExchangeStatusFactory(
)
}
- operator fun invoke() = combine(
+ suspend operator fun invoke() = combine(
flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrency.id),
flow2 = getWalletCryptoCurrencies().conflate(),
) { savedTransactions, cryptoCurrenciesStatusList ->
@@ -95,9 +95,10 @@ internal class ExchangeStatusFactory(
return swapRepository.getExchangeStatus(txId)
.fold(
ifLeft = { null },
- ifRight = {
- sendStatusUpdateAnalytics(it)
- it
+ ifRight = { statusModel ->
+ sendStatusUpdateAnalytics(statusModel)
+ swapTransactionRepository.storeTransactionState(txId, statusModel)
+ statusModel
},
)
}
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt
index 1e23353d4a..1c3f77a105 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt
@@ -222,6 +222,7 @@ internal class TokenDetailsViewModel @Inject constructor(
.distinctUntilChanged()
.filterNot { it.isEmpty() }
.onEach { swapTxs ->
+ updateSwapTx(swapTxs)
swapTxStatusTaskScheduler.scheduleTask(
viewModelScope,
PeriodicTask(
@@ -231,18 +232,8 @@ internal class TokenDetailsViewModel @Inject constructor(
exchangeStatusFactory.updateSwapTxStatuses(swapTxs)
}
},
- onSuccess = { updatedTxs ->
- val config = uiState.bottomSheetConfig
- val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig
- val currentTx = updatedTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId }
- uiState = uiState.copy(
- swapTxs = updatedTxs,
- bottomSheetConfig = currentTx?.let(
- stateFactory::updateStateWithExchangeStatusBottomSheet,
- ) ?: config,
- )
- },
- onError = {},
+ onSuccess = ::updateSwapTx,
+ onError = { /* no-op */ },
),
)
}
@@ -252,6 +243,18 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
+ private fun updateSwapTx(swapTxs: PersistentList) {
+ val config = uiState.bottomSheetConfig
+ val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig
+ val currentTx = swapTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId }
+ uiState = uiState.copy(
+ swapTxs = swapTxs,
+ bottomSheetConfig = currentTx?.let(
+ stateFactory::updateStateWithExchangeStatusBottomSheet,
+ ) ?: config,
+ )
+ }
+
/**
* @param refresh - invalidate cache and get data from remote
* @param showItemsLoading - show loading items placeholder.