Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-20 14:43:31 +03:00
commit c520cb1463
21 changed files with 246 additions and 60 deletions

View file

@ -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
}
}

View file

@ -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") }

View file

@ -194,6 +194,7 @@
<string name="exchange_tokens_available_tokens_header">Мои токены</string>
<string name="exchange_tokens_empty_tokens">У вас нет добавленных токенов. Добавьте токены для обмена</string>
<string name="exchange_tokens_unavailable_tokens_header">Недоступен для обмена с %s</string>
<string name="express_cex_fee_explanation">Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес</string>
<string name="express_cex_status_button_title">Статус</string>
<string name="express_choose_providers_subtitle">Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами</string>
<string name="express_choose_providers_title">Выберите провайдера</string>

View file

@ -193,6 +193,7 @@
<string name="exchange_tokens_available_tokens_header">My tokens</string>
<string name="exchange_tokens_empty_tokens">You haven\'t added any tokens yet. Add tokens via Market to swap</string>
<string name="exchange_tokens_unavailable_tokens_header">Cannot be swapped for %s</string>
<string name="express_cex_fee_explanation">Additionally, the network fee for sending the exchanged funds back to your address is included in the rate</string>
<string name="express_cex_status_button_title">Status</string>
<string name="express_choose_providers_subtitle">Providers facilitate transactions, ensuring smooth and efficient token swaps</string>
<string name="express_choose_providers_title">Choose provider</string>

View file

@ -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 ->

View file

@ -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<SavedSwapTransactionListModel>? = 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<List<SavedSwapTransactionListModel>?> {
val txStatuses = appPreferencesStore.getObjectMap<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
return appPreferencesStore.getObjectList<SavedSwapTransactionListModel>(
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<SavedSwapTransactionListModel>? = 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<ExchangeStatusModel>(
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<SavedLastSwappedCryptoCurrency>(
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<ExchangeStatusModel>(
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,
)
}
}
}
}

View file

@ -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

View file

@ -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
}

View file

@ -37,6 +37,7 @@ sealed interface SwapState {
data class SwapError(
val fromTokenInfo: TokenSwapInfo,
val error: DataError,
val includeFeeInAmount: IncludeFeeInAmount,
) : SwapState
}

View file

@ -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()

View file

@ -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<SwapProvider, SwapState> {
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(

View file

@ -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<List<SavedSwapTransactionListModel>?>
@ -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?

View file

@ -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,
)
}

View file

@ -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()

View file

@ -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 = {},
),

View file

@ -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 = {},
)

View file

@ -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<SwapWarning>()
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 {

View file

@ -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,
),

View file

@ -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),

View file

@ -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
},
)
}

View file

@ -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<SwapTransactionsState>) {
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.