Updated on 2026-08-14
This commit is contained in:
commit
c520cb1463
21 changed files with 246 additions and 60 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ sealed interface SwapState {
|
|||
data class SwapError(
|
||||
val fromTokenInfo: TokenSwapInfo,
|
||||
val error: DataError,
|
||||
val includeFeeInAmount: IncludeFeeInAmount,
|
||||
) : SwapState
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue