Updated on 2026-08-14
This commit is contained in:
parent
8ef0c9f5a6
commit
6d697e05c1
15 changed files with 217 additions and 83 deletions
|
|
@ -4,6 +4,7 @@ import androidx.core.os.bundleOf
|
|||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
|
@ -155,11 +156,11 @@ class TradeCryptoMiddleware {
|
|||
return when (this) {
|
||||
is Currency.Blockchain -> {
|
||||
SwapCurrency.NativeToken(
|
||||
id = blockchain.id,
|
||||
id = blockchain.toCoinId(),
|
||||
name = this.currencyName,
|
||||
symbol = this.currencySymbol,
|
||||
networkId = this.blockchain.toNetworkId(),
|
||||
logoUrl = getIconUrl(this.blockchain.toNetworkId()),
|
||||
logoUrl = getIconUrl(this.blockchain.toCoinId()),
|
||||
)
|
||||
}
|
||||
is Currency.Token -> SwapCurrency.NonNativeToken(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.blockchain.common.WalletManagerFactory
|
|||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.lib.crypto.UserWalletManager
|
||||
import com.tangem.lib.crypto.models.Currency
|
||||
|
|
@ -59,6 +60,16 @@ class UserWalletManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getNativeTokenForNetwork(networkId: String): Currency {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
return NativeToken(
|
||||
id = blockchain.toCoinId(),
|
||||
name = blockchain.fullName,
|
||||
symbol = blockchain.currency,
|
||||
networkId = networkId,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getWalletId(): String {
|
||||
return appStateHolder.getActualCard()?.let {
|
||||
UserWalletIdBuilder.card(it)
|
||||
|
|
@ -148,11 +159,6 @@ class UserWalletManagerImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getCurrencyByNetworkId(networkId: String): String {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
return blockchain.currency
|
||||
}
|
||||
|
||||
private fun addNativeTokenToWalletAction(token: NativeToken, card: CardDTO): Action {
|
||||
val blockchain = Blockchain.fromNetworkId(token.networkId)
|
||||
val scanResponse = appStateHolder.scanResponse
|
||||
|
|
|
|||
|
|
@ -34,4 +34,42 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"DASH" -> R.drawable.img_dash_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
@DrawableRes
|
||||
fun getActiveIconResByCoinId(coinId: String, networkId: String): Int {
|
||||
return when (coinId) {
|
||||
"binancecoin" -> R.drawable.img_bsc_22
|
||||
"bitcoin" -> R.drawable.img_btc_22
|
||||
"bitcoin-cash" -> R.drawable.img_btc_cash_22
|
||||
"ethereum" -> {
|
||||
when (networkId) {
|
||||
"ethereum" -> R.drawable.img_eth_22
|
||||
"arbitrum-one" -> R.drawable.img_arbitrum_22
|
||||
"optimistic-ethereum" -> R.drawable.img_optimism_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
"ethereum-classic" -> R.drawable.img_eth_classic_22
|
||||
"stellar" -> R.drawable.img_stellar_22
|
||||
"cardano" -> R.drawable.img_cardano_22
|
||||
"matic-network" -> R.drawable.img_polygon_22
|
||||
"avalanche-2" -> R.drawable.img_avalanche_22
|
||||
"solana" -> R.drawable.img_solana_22
|
||||
"fantom" -> R.drawable.img_fantom_22
|
||||
"tron" -> R.drawable.img_tron_22
|
||||
"polkadot" -> R.drawable.img_polkadot_22
|
||||
"litecoin" -> R.drawable.img_litecoin_22
|
||||
"rootstock" -> R.drawable.img_rsk_22
|
||||
"tezos" -> R.drawable.img_tezos_22
|
||||
"ripple" -> R.drawable.img_xrp_22
|
||||
"dogecoin" -> R.drawable.img_dogecoin_22
|
||||
"xdai" -> R.drawable.img_gnosis_22
|
||||
"ethereum-pow-iou" -> R.drawable.img_eth_pow_22
|
||||
"ethereumfair" -> R.drawable.img_eth_fair_22
|
||||
"kusama" -> R.drawable.img_kusama_22
|
||||
"dash" -> R.drawable.img_dash_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -31,11 +31,12 @@ class TokensConverter @Inject constructor() : Converter<CoinsResponse.Coin, Curr
|
|||
}
|
||||
|
||||
private fun getSmallIconUrl(coin: String): String {
|
||||
return "$DEFAULT_IMAGE_HOST$SMALL_ICON_PATH/$coin.png"
|
||||
return "$DEFAULT_IMAGE_HOST$LARGE_ICON_PATH/$coin.png"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
|
||||
private const val SMALL_ICON_PATH = "small"
|
||||
private const val LARGE_ICON_PATH = "large"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ApproveModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
|
|
@ -27,7 +28,7 @@ interface SwapInteractor {
|
|||
* @param searchQuery string query for search
|
||||
* @return [FoundTokensState] that contains list of tokens matching condition query
|
||||
*/
|
||||
suspend fun onSearchToken(networkId: String, searchQuery: String): FoundTokensState
|
||||
suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensState
|
||||
|
||||
/**
|
||||
* Find specific token by id, null if not found
|
||||
|
|
@ -91,4 +92,11 @@ interface SwapInteractor {
|
|||
): TxState
|
||||
|
||||
fun getTokenDecimals(token: Currency): Int
|
||||
|
||||
/**
|
||||
* Returns token in wallet balance
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
fun getTokenBalance(token: Currency): SwapAmount
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun onSearchToken(networkId: String, searchQuery: String): FoundTokensState {
|
||||
override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensState {
|
||||
val searchQueryLowerCase = searchQuery.lowercase()
|
||||
val tokensInWallet = cache.getInWalletTokens()
|
||||
.filter {
|
||||
|
|
@ -233,6 +233,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getTokenBalance(token: Currency): SwapAmount {
|
||||
return userWalletManager.getCurrentWalletTokensBalance(token.networkId)[token.symbol]?.let {
|
||||
SwapAmount(it.value, it.decimals)
|
||||
} ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token))
|
||||
}
|
||||
|
||||
private fun selectToToken(
|
||||
initialToken: Currency,
|
||||
tokensInWallet: List<Currency>,
|
||||
|
|
@ -242,9 +248,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
tokensInWallet.firstOrNull { it.symbol != initialToken.symbol }
|
||||
?: loadedTokens.first { it.symbol != initialToken.symbol }
|
||||
} else {
|
||||
val findUsdt = loadedTokens.firstOrNull { it.symbol == "USDT" && it.symbol != initialToken.symbol }
|
||||
val findUsdt = loadedTokens.firstOrNull { it.symbol == USDT_SYMBOL && it.symbol != initialToken.symbol }
|
||||
if (findUsdt == null) {
|
||||
val findUsdc = loadedTokens.firstOrNull { it.symbol == "USDC" && it.symbol != initialToken.symbol }
|
||||
val findUsdc = loadedTokens.firstOrNull { it.symbol == USDC_SYMBOL && it.symbol != initialToken.symbol }
|
||||
findUsdc ?: loadedTokens.first { it.symbol != initialToken.symbol }
|
||||
} else {
|
||||
findUsdt
|
||||
|
|
@ -315,8 +321,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
SwapAmount(it.value, it.decimals)
|
||||
}
|
||||
return SwapState.EmptyAmountState(
|
||||
fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: "",
|
||||
toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: "",
|
||||
fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(),
|
||||
toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -342,17 +348,24 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val quoteDataModel = quotes.dataModel
|
||||
if (quoteDataModel != null) {
|
||||
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
|
||||
val fee = transactionManager.calculateFee(
|
||||
networkId = networkId,
|
||||
estimatedGas = quoteDataModel.estimatedGas,
|
||||
gasPrice = transactionData.gasPrice,
|
||||
)
|
||||
val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee)
|
||||
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
|
||||
amount = fee,
|
||||
decimals = transactionManager.getNativeTokenDecimals(networkId),
|
||||
currency = userWalletManager.getNetworkCurrency(networkId),
|
||||
) + feeFiat
|
||||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
fromTokenAmount = quoteDataModel.fromTokenAmount,
|
||||
toTokenAmount = quoteDataModel.toTokenAmount,
|
||||
fee = transactionManager.calculateFee(
|
||||
networkId = networkId,
|
||||
estimatedGas = quoteDataModel.estimatedGas,
|
||||
gasPrice = transactionData.gasPrice,
|
||||
),
|
||||
formattedFee = formattedFee,
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
swapDataModel = null,
|
||||
)
|
||||
|
|
@ -362,6 +375,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
quotesLoadedState = swapState,
|
||||
estimatedGas = quoteDataModel.estimatedGas,
|
||||
transactionData = transactionData,
|
||||
formattedFee = formattedFee,
|
||||
)
|
||||
} else {
|
||||
return SwapState.SwapError(quotes.error)
|
||||
|
|
@ -369,6 +383,20 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getFormattedFiatFee(
|
||||
networkId: String,
|
||||
fromTokenId: String,
|
||||
toTokenId: String,
|
||||
fee: BigDecimal,
|
||||
): String {
|
||||
val appCurrency = userWalletManager.getUserAppCurrency()
|
||||
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
|
||||
val rates = repository.getRates(appCurrency.code, listOf(fromTokenId, toTokenId, nativeToken.id))
|
||||
return rates[nativeToken.id]?.toBigDecimal()?.let { rate ->
|
||||
" (${fee.toFiatString(rate, appCurrency.symbol)})"
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load swap data calls only if spend is allowed for token contract address
|
||||
*/
|
||||
|
|
@ -392,17 +420,24 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
).let {
|
||||
val swapData = it.dataModel
|
||||
if (swapData != null) {
|
||||
val fee = transactionManager.calculateFee(
|
||||
networkId = networkId,
|
||||
estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS,
|
||||
gasPrice = swapData.transaction.gasPrice,
|
||||
)
|
||||
val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee)
|
||||
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
|
||||
amount = fee,
|
||||
decimals = transactionManager.getNativeTokenDecimals(networkId),
|
||||
currency = userWalletManager.getNetworkCurrency(networkId),
|
||||
) + feeFiat
|
||||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
toToken = toToken,
|
||||
fromTokenAmount = swapData.fromTokenAmount,
|
||||
toTokenAmount = swapData.toTokenAmount,
|
||||
fee = transactionManager.calculateFee(
|
||||
networkId = networkId,
|
||||
estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS,
|
||||
gasPrice = swapData.transaction.gasPrice,
|
||||
),
|
||||
formattedFee = formattedFee,
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
swapDataModel = swapData,
|
||||
)
|
||||
|
|
@ -422,12 +457,13 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
toToken: Currency,
|
||||
fromTokenAmount: SwapAmount,
|
||||
toTokenAmount: SwapAmount,
|
||||
fee: BigDecimal,
|
||||
formattedFee: String,
|
||||
preparedSwapConfigState: PreparedSwapConfigState,
|
||||
swapDataModel: SwapDataModel?,
|
||||
): SwapState.QuotesLoadedState {
|
||||
val appCurrency = userWalletManager.getUserAppCurrency()
|
||||
val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id))
|
||||
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
|
||||
val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id, nativeToken.id))
|
||||
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId)
|
||||
val fromTokenBalance = tokensBalance[fromToken.symbol]?.let {
|
||||
amountFormatter.formatProxyAmountToUI(it, "")
|
||||
|
|
@ -438,7 +474,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
return SwapState.QuotesLoadedState(
|
||||
fromTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = fromTokenAmount,
|
||||
tokenId = fromToken.id,
|
||||
coinId = fromToken.id,
|
||||
tokenWalletBalance = fromTokenBalance ?: ZERO_BALANCE,
|
||||
tokenFiatBalance = fromTokenAmount.value.toFiatString(
|
||||
rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
|
|
@ -447,18 +483,14 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
),
|
||||
toTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = toTokenAmount,
|
||||
tokenId = toToken.id,
|
||||
coinId = toToken.id,
|
||||
tokenWalletBalance = toTokenBalance ?: ZERO_BALANCE,
|
||||
tokenFiatBalance = toTokenAmount.value.toFiatString(
|
||||
rates[toToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
|
||||
appCurrency.symbol,
|
||||
),
|
||||
),
|
||||
fee = amountFormatter.formatBigDecimalAmountToUI(
|
||||
amount = fee,
|
||||
decimals = transactionManager.getNativeTokenDecimals(networkId),
|
||||
currency = userWalletManager.getCurrencyByNetworkId(networkId),
|
||||
),
|
||||
fee = formattedFee,
|
||||
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
|
||||
preparedSwapConfigState = preparedSwapConfigState,
|
||||
swapDataModel = swapDataModel,
|
||||
|
|
@ -471,6 +503,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
quotesLoadedState: SwapState.QuotesLoadedState,
|
||||
estimatedGas: Int,
|
||||
transactionData: ApproveModel,
|
||||
formattedFee: String,
|
||||
): SwapState.QuotesLoadedState {
|
||||
if (allowPermissionsHandler.isAddressAllowanceInProgress(getTokenAddress(fromToken))) {
|
||||
return quotesLoadedState.copy(
|
||||
|
|
@ -479,15 +512,11 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
return quotesLoadedState.copy(
|
||||
permissionState = PermissionDataState.PermissionReadyForRequest(
|
||||
currency = userWalletManager.getCurrencyByNetworkId(networkId),
|
||||
amount = "∞",
|
||||
currency = fromToken.symbol,
|
||||
amount = INFINITY_SYMBOL,
|
||||
walletAddress = getWalletAddress(networkId),
|
||||
spenderAddress = transactionData.toAddress,
|
||||
fee = transactionManager.calculateFee(
|
||||
networkId = networkId,
|
||||
gasPrice = transactionData.gasPrice,
|
||||
estimatedGas = estimatedGas,
|
||||
).toPlainString(),
|
||||
fee = formattedFee,
|
||||
requestApproveData = RequestApproveStateData(
|
||||
estimatedGas = estimatedGas,
|
||||
approveModel = transactionData,
|
||||
|
|
@ -497,10 +526,14 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isBalanceEnough(fromToken: Currency, networkId: String, amount: SwapAmount, fee: BigDecimal?): Boolean {
|
||||
/** to compare [BigDecimal] use only comparator */
|
||||
val balance = userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value
|
||||
?: BigDecimal.ZERO
|
||||
return balance > amount.value.plus(fee ?: BigDecimal.ZERO)
|
||||
return if (fromToken is Currency.NonNativeToken) {
|
||||
(userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value
|
||||
?: BigDecimal.ZERO) >= amount.value
|
||||
} else {
|
||||
/** to compare [BigDecimal] use only comparator */
|
||||
(userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value
|
||||
?: BigDecimal.ZERO) > amount.value.plus(fee ?: BigDecimal.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWalletAddress(networkId: String): String {
|
||||
|
|
@ -544,5 +577,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
private const val TWENTY_FIVE_PERCENTS = 25
|
||||
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.5
|
||||
private const val USDT_SYMBOL = "USDT"
|
||||
private const val USDC_SYMBOL = "USDC"
|
||||
private const val INFINITY_SYMBOL = "∞"
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ sealed class PermissionDataState {
|
|||
|
||||
data class TokenSwapInfo(
|
||||
val tokenAmount: SwapAmount,
|
||||
val tokenId: String,
|
||||
val coinId: String,
|
||||
val tokenWalletBalance: String,
|
||||
val tokenFiatBalance: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ data class SwapStateHolder(
|
|||
val sendCardData: SwapCardData,
|
||||
val receiveCardData: SwapCardData,
|
||||
val networkCurrency: String,
|
||||
val networkId: String,
|
||||
val fee: FeeState = FeeState.Empty,
|
||||
val warnings: List<SwapWarning> = emptyList(),
|
||||
val alert: SwapWarning.GenericWarning? = null,
|
||||
|
|
@ -29,8 +30,8 @@ data class SwapCardData(
|
|||
val type: TransactionCardType,
|
||||
val amount: String?,
|
||||
val amountEquivalent: String?,
|
||||
val coinId: String?,
|
||||
val tokenIconUrl: String,
|
||||
val tokenId: String,
|
||||
val tokenCurrency: String,
|
||||
val balance: String,
|
||||
@DrawableRes val networkIconRes: Int? = null,
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ data class UiActions(
|
|||
val onGivePermissionClick: () -> Unit,
|
||||
val onChangeCardsClicked: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onMaxAmountSelected: () -> Unit,
|
||||
)
|
||||
|
|
@ -29,13 +29,14 @@ class StateBuilder(val actions: UiActions) {
|
|||
|
||||
fun createInitialLoadingState(initialCurrency: Currency): SwapStateHolder {
|
||||
return SwapStateHolder(
|
||||
networkId = initialCurrency.networkId,
|
||||
sendCardData = SwapCardData(
|
||||
type = TransactionCardType.SendCard(actions.onAmountChanged),
|
||||
amount = null,
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = initialCurrency.logoUrl,
|
||||
tokenCurrency = initialCurrency.symbol,
|
||||
tokenId = initialCurrency.id,
|
||||
coinId = initialCurrency.id,
|
||||
canSelectAnotherToken = false,
|
||||
balance = "",
|
||||
),
|
||||
|
|
@ -47,7 +48,7 @@ class StateBuilder(val actions: UiActions) {
|
|||
tokenCurrency = "",
|
||||
canSelectAnotherToken = false,
|
||||
balance = "",
|
||||
tokenId = "",
|
||||
coinId = null,
|
||||
),
|
||||
fee = FeeState.Loading,
|
||||
networkCurrency = initialCurrency.symbol,
|
||||
|
|
@ -55,6 +56,8 @@ class StateBuilder(val actions: UiActions) {
|
|||
onRefresh = {},
|
||||
onBackClicked = actions.onBackClicked,
|
||||
onChangeCardsClicked = actions.onChangeCardsClicked,
|
||||
onMaxAmountSelected = actions.onMaxAmountSelected,
|
||||
updateInProgress = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,10 +71,10 @@ class StateBuilder(val actions: UiActions) {
|
|||
sendCardData = SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amount = uiStateHolder.sendCardData.amount,
|
||||
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = fromToken.logoUrl,
|
||||
tokenCurrency = fromToken.symbol,
|
||||
tokenId = fromToken.id,
|
||||
coinId = fromToken.id,
|
||||
canSelectAnotherToken = mainTokenId != fromToken.id,
|
||||
balance = "",
|
||||
),
|
||||
|
|
@ -81,13 +84,14 @@ class StateBuilder(val actions: UiActions) {
|
|||
amountEquivalent = null,
|
||||
tokenIconUrl = toToken.logoUrl,
|
||||
tokenCurrency = toToken.symbol,
|
||||
tokenId = toToken.id,
|
||||
coinId = toToken.id,
|
||||
canSelectAnotherToken = mainTokenId != toToken.id,
|
||||
balance = "",
|
||||
),
|
||||
fee = FeeState.Loading,
|
||||
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
|
||||
permissionState = uiStateHolder.permissionState,
|
||||
updateInProgress = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +115,10 @@ class StateBuilder(val actions: UiActions) {
|
|||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amount = quoteModel.fromTokenInfo.tokenAmount.formatToUIRepresentation(),
|
||||
amount = uiStateHolder.sendCardData.amount,
|
||||
amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance,
|
||||
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
|
||||
tokenId = quoteModel.fromTokenInfo.tokenId,
|
||||
coinId = quoteModel.fromTokenInfo.coinId,
|
||||
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
|
||||
balance = quoteModel.fromTokenInfo.tokenWalletBalance,
|
||||
|
|
@ -124,7 +128,7 @@ class StateBuilder(val actions: UiActions) {
|
|||
amount = quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation(),
|
||||
amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance,
|
||||
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
|
||||
tokenId = quoteModel.toTokenInfo.tokenId,
|
||||
coinId = quoteModel.toTokenInfo.coinId,
|
||||
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
|
||||
balance = quoteModel.toTokenInfo.tokenWalletBalance,
|
||||
|
|
@ -140,6 +144,7 @@ class StateBuilder(val actions: UiActions) {
|
|||
loading = false,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
updateInProgress = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -151,9 +156,9 @@ class StateBuilder(val actions: UiActions) {
|
|||
sendCardData = SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amount = uiStateHolder.sendCardData.amount,
|
||||
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
|
||||
amountEquivalent = "",
|
||||
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
|
||||
tokenId = uiStateHolder.sendCardData.tokenId,
|
||||
coinId = uiStateHolder.sendCardData.coinId,
|
||||
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
|
||||
balance = emptyAmountState.fromTokenWalletBalance,
|
||||
|
|
@ -161,9 +166,9 @@ class StateBuilder(val actions: UiActions) {
|
|||
receiveCardData = SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amount = "0",
|
||||
amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent,
|
||||
amountEquivalent = "",
|
||||
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
|
||||
tokenId = uiStateHolder.receiveCardData.tokenId,
|
||||
coinId = uiStateHolder.receiveCardData.coinId,
|
||||
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
|
||||
balance = emptyAmountState.toTokenWalletBalance,
|
||||
|
|
@ -174,6 +179,7 @@ class StateBuilder(val actions: UiActions) {
|
|||
loading = false,
|
||||
onClick = { },
|
||||
),
|
||||
updateInProgress = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +257,7 @@ class StateBuilder(val actions: UiActions) {
|
|||
message = null,
|
||||
onClick = onAlertClick,
|
||||
),
|
||||
updateInProgress = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -302,4 +309,10 @@ class StateBuilder(val actions: UiActions) {
|
|||
const val ADDRESS_FIRST_PART_LENGTH = 7
|
||||
const val ADDRESS_SECOND_PART_LENGTH = 4
|
||||
}
|
||||
|
||||
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
updateInProgress = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.material.ModalBottomSheetLayout
|
|||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -34,6 +35,10 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
|
|||
)
|
||||
} else {
|
||||
// Required "else" block to prevent compose crash
|
||||
// always close BS if its empty, cause user should not see this
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch { bottomSheetState.hide() }
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import com.tangem.core.ui.components.SmallInfoCardWithWarning
|
|||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.getActiveIconResByCoinId
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.FeeState
|
||||
import com.tangem.feature.swap.models.SwapButton
|
||||
|
|
@ -137,7 +137,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable { state.onMaxAmountSelected }
|
||||
.clickable { state.onMaxAmountSelected?.invoke() }
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing14,
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
|
|
@ -170,7 +170,9 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
tokenIconUrl = state.sendCardData.tokenIconUrl,
|
||||
tokenCurrency = state.sendCardData.tokenCurrency,
|
||||
networkIconRes = state.sendCardData.networkIconRes,
|
||||
iconPlaceholder = getActiveIconRes(state.sendCardData.tokenId),
|
||||
iconPlaceholder = state.sendCardData.coinId?.let {
|
||||
getActiveIconResByCoinId(it, state.networkId)
|
||||
},
|
||||
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
|
||||
)
|
||||
SpacerH16()
|
||||
|
|
@ -182,7 +184,9 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
tokenIconUrl = state.receiveCardData.tokenIconUrl,
|
||||
tokenCurrency = state.receiveCardData.tokenCurrency,
|
||||
networkIconRes = state.receiveCardData.networkIconRes,
|
||||
iconPlaceholder = getActiveIconRes(state.receiveCardData.tokenId),
|
||||
iconPlaceholder = state.receiveCardData.coinId?.let {
|
||||
getActiveIconResByCoinId(it, state.networkId)
|
||||
},
|
||||
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) {
|
||||
state.onSelectTokenClick
|
||||
} else {
|
||||
|
|
@ -190,13 +194,13 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
},
|
||||
)
|
||||
}
|
||||
SwapButton()
|
||||
SwapButton(state)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SwapButton() {
|
||||
private fun SwapButton(state: SwapStateHolder) {
|
||||
Card(
|
||||
elevation = TangemTheme.dimens.elevation3,
|
||||
shape = CircleShape,
|
||||
|
|
@ -313,7 +317,7 @@ private val sendCard = SwapCardData(
|
|||
networkIconRes = R.drawable.img_polygon_22,
|
||||
canSelectAnotherToken = false,
|
||||
balance = "123",
|
||||
tokenId = "",
|
||||
coinId = "",
|
||||
)
|
||||
|
||||
private val receiveCard = SwapCardData(
|
||||
|
|
@ -325,10 +329,11 @@ private val receiveCard = SwapCardData(
|
|||
networkIconRes = R.drawable.img_polygon_22,
|
||||
canSelectAnotherToken = true,
|
||||
balance = "33333",
|
||||
tokenId = "",
|
||||
coinId = "",
|
||||
)
|
||||
|
||||
private val state = SwapStateHolder(
|
||||
networkId = "ethereum",
|
||||
sendCardData = sendCard,
|
||||
receiveCardData = receiveCard,
|
||||
fee = FeeState.Loaded(fee = "0.155 MATIC (0.14 $)"),
|
||||
|
|
|
|||
|
|
@ -351,13 +351,14 @@ fun Token(
|
|||
.align(Alignment.BottomStart)
|
||||
.size(TangemTheme.dimens.size36)
|
||||
|
||||
val data = tokenIconUrl.ifEmpty {
|
||||
iconPlaceholder
|
||||
}
|
||||
SubcomposeAsyncImage(
|
||||
modifier = tokenImageModifier,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(tokenIconUrl)
|
||||
.data(data)
|
||||
.crossfade(true)
|
||||
.error(iconPlaceholder ?: 0)
|
||||
.placeholder(iconPlaceholder ?: 0)
|
||||
.build(),
|
||||
loading = { TokenImageShimmer(modifier = tokenImageModifier) },
|
||||
// error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, tokenCurrency) },
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
|
|
@ -23,6 +24,7 @@ import com.tangem.feature.swap.ui.StateBuilder
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import com.tangem.utils.toFormattedString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
|
@ -125,6 +127,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
return PeriodicTask(
|
||||
UPDATE_DELAY,
|
||||
task = {
|
||||
uiState = stateBuilder.createSilentLoadState(uiState)
|
||||
runCatching(dispatchers.io) {
|
||||
dataState = dataState.copy(
|
||||
amount = amount,
|
||||
|
|
@ -244,7 +247,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
private fun onSearchEntered(searchQuery: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.onSearchToken(dataState.networkId, searchQuery)
|
||||
swapInteractor.searchTokens(dataState.networkId, searchQuery)
|
||||
}
|
||||
.onSuccess {
|
||||
updateTokensState(it)
|
||||
|
|
@ -283,22 +286,36 @@ internal class SwapViewModel @Inject constructor(
|
|||
toCurrency = newToToken,
|
||||
)
|
||||
isOrderReversed = !isOrderReversed
|
||||
lastAmount.value = cutAmountWithDecimals(swapInteractor.getTokenDecimals(newFromToken), lastAmount.value)
|
||||
uiState = stateBuilder.updateSwapAmount(uiState, lastAmount.value)
|
||||
startLoadingQuotes(newFromToken, newToToken, lastAmount.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAmountChanged(value: String) {
|
||||
uiState = stateBuilder.updateSwapAmount(uiState, value)
|
||||
lastAmount.value = value
|
||||
amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
|
||||
val fromToken = dataState.fromCurrency
|
||||
val toToken = dataState.toCurrency
|
||||
if (fromToken != null && toToken != null) {
|
||||
val fromToken = dataState.fromCurrency
|
||||
val toToken = dataState.toCurrency
|
||||
if (fromToken != null && toToken != null) {
|
||||
val cutValue = cutAmountWithDecimals(swapInteractor.getTokenDecimals(fromToken), value)
|
||||
uiState = stateBuilder.updateSwapAmount(uiState, cutValue)
|
||||
lastAmount.value = cutValue
|
||||
amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
|
||||
startLoadingQuotes(fromToken, toToken, lastAmount.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMaxAmountClicked() {
|
||||
dataState.fromCurrency?.let {
|
||||
val balance = swapInteractor.getTokenBalance(it)
|
||||
onAmountChanged(balance.formatToUIRepresentation())
|
||||
}
|
||||
}
|
||||
|
||||
private fun cutAmountWithDecimals(maxDecimals: Int, amount: String): String {
|
||||
return amount.toBigDecimalOrNull()?.toFormattedString(maxDecimals) ?: INITIAL_AMOUNT
|
||||
}
|
||||
|
||||
private fun createUiActions(): UiActions {
|
||||
return UiActions(
|
||||
onSearchEntered = { onSearchEntered(it) },
|
||||
|
|
@ -308,12 +325,13 @@ internal class SwapViewModel @Inject constructor(
|
|||
onGivePermissionClick = { givePermissionsToSwap() },
|
||||
onChangeCardsClicked = { onChangeCardsClicked() },
|
||||
onBackClicked = { onSearchEntered("") },
|
||||
onMaxAmountSelected = { onMaxAmountClicked() },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_AMOUNT = ""
|
||||
private const val UPDATE_DELAY = 60000L
|
||||
private const val UPDATE_DELAY = 10000L
|
||||
private const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ interface UserWalletManager {
|
|||
*/
|
||||
suspend fun getUserTokens(networkId: String): List<Currency>
|
||||
|
||||
fun getNativeTokenForNetwork(networkId: String): Currency
|
||||
|
||||
/**
|
||||
* Returns user walletId
|
||||
*/
|
||||
|
|
@ -51,16 +53,14 @@ interface UserWalletManager {
|
|||
|
||||
fun getNativeTokenBalance(networkId: String): ProxyAmount?
|
||||
|
||||
/**
|
||||
* @param networkId
|
||||
* @return currency name
|
||||
*/
|
||||
fun getNetworkCurrency(networkId: String): String
|
||||
|
||||
/**
|
||||
* Returns selected app currency
|
||||
*/
|
||||
fun getUserAppCurrency(): ProxyFiatCurrency
|
||||
|
||||
/**
|
||||
* @param networkId
|
||||
* @return currency name
|
||||
*/
|
||||
fun getCurrencyByNetworkId(networkId: String): String
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue