Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-25 14:16:33 +03:00
parent 8ef0c9f5a6
commit 6d697e05c1
15 changed files with 217 additions and 83 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,4 +8,5 @@ data class UiActions(
val onGivePermissionClick: () -> Unit,
val onChangeCardsClicked: () -> Unit,
val onBackClicked: () -> Unit,
val onMaxAmountSelected: () -> Unit,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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