diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index b5726a45bf..095b8f4ea0 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b5726a45bf51b7763c5967afae4d3d687676240b +Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 01fe1e3deb..3e8529f150 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -176,9 +176,9 @@ class WalletConnectSdkHelper { is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2")) is Result.Failure -> { (gasLimitResult.error as? Throwable)?.let { Timber.e(it, "getGasLimit failed") } - BigDecimal(DEFAULT_MAX_GASLIMIT) // Set high gasLimit if not provided + DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided } - else -> BigDecimal(DEFAULT_MAX_GASLIMIT) // Set high gasLimit if not provided + else -> DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 4266b2fa73..cea126bf75 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -35,7 +35,8 @@ internal class DefaultLegacyWalletConnectRepository( private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() override val activeSessions: Flow> = _activeSessions - private var currentSessions: List = emptyList() + override var currentSessions: List = emptyList() + private set /** * @param projectId Project ID at https://cloud.walletconnect.com/ diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 9096e879e0..616643a960 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -9,6 +9,8 @@ interface LegacyWalletConnectRepository { val activeSessions: Flow> + val currentSessions: List + fun init(projectId: String) fun setUserNamespaces(userNamespaces: Map>) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 66f8b5d16c..1f5dce9663 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -132,6 +132,7 @@ class WalletConnectInteractor( runCatching { if (accounts.isEmpty()) return isWalletConnectReadyForDeepLinks = true + if (deeplinkStack.empty()) return val lastDeeplink = deeplinkStack.pop() store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink)) }.onFailure { @@ -361,6 +362,19 @@ class WalletConnectInteractor( * @param deeplink deeplink to handle */ fun addDeeplink(deeplink: String) { + val deeplinkRegex = Regex(WC_PARAM_REGEX) + val matched = deeplinkRegex.findAll(deeplink) + val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull() + + val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session -> + session.topic == sessionTopic + } + + if (isAlreadyActiveSessionTopic && sessionTopic != null) { + Timber.i("WC already has an active session topic: $deeplink") + return + } + if (isWalletConnectReadyForDeepLinks) { store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink)) } else { @@ -406,7 +420,9 @@ class WalletConnectInteractor( return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) } - companion object { - private const val WC_SCHEME = "wc" + private companion object { + const val WC_SCHEME = "wc" + const val WC_TOPIC_QUERY_NAME = "sessionTopic" + const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 203a2f983f..38c890dc29 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -1,5 +1,7 @@ package com.tangem.tap.features.home.redux +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess @@ -62,8 +64,13 @@ private fun handleHomeAction(action: Action) { } is HomeAction.GoToShop -> { Analytics.send(Shop.ScreenOpened()) - store.dispatchOpenUrl(NEW_BUY_WALLET_URL) - + Firebase.analytics.appInstanceId + .addOnSuccessListener { + store.dispatchOpenUrl("$NEW_BUY_WALLET_URL&app_instance_id=$it") + } + .addOnFailureListener { + store.dispatchOpenUrl(NEW_BUY_WALLET_URL) + } // disabled for now in task [REDACTED_JIRA] // when (action.userCountryCode) { // RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index c199b92e7b..cfd1fc53f2 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -39,7 +39,7 @@ class TransactionManagerImpl( @Throws(IllegalStateException::class) override suspend fun getFee( networkId: String, - amountToSend: BigDecimal, + amountToSend: Amount, currencyToSend: Currency, destinationAddress: String, increaseBy: Int?, @@ -52,7 +52,7 @@ class TransactionManagerImpl( if (walletManager is EthereumOptimisticRollupWalletManager) { return getFeeForOptimismBlockchain( walletManager = walletManager, - amount = createAmount(amountToSend, currencyToSend, blockchain), + amount = amountToSend, destinationAddress = destinationAddress, data = data, ) @@ -61,7 +61,6 @@ class TransactionManagerImpl( walletManager = walletManager, blockchain = blockchain, amountToSend = amountToSend, - currency = currencyToSend, destinationAddress = destinationAddress, data = data, increaseBy = increaseBy, @@ -70,8 +69,6 @@ class TransactionManagerImpl( return getFeeForBlockchain( walletManager = walletManager, amountToSend = amountToSend, - currency = currencyToSend, - blockchain = blockchain, destinationAddress = destinationAddress, ) } @@ -87,13 +84,11 @@ class TransactionManagerImpl( private suspend fun getFeeForBlockchain( walletManager: WalletManager, - amountToSend: BigDecimal, - currency: Currency, - blockchain: Blockchain, + amountToSend: Amount, destinationAddress: String, ): ProxyFees { val fee = (walletManager as? TransactionSender)?.getFee( - amount = createAmount(amountToSend, currency, blockchain), + amount = amountToSend, destination = destinationAddress, ) ?: error("Cannot cast to TransactionSender") return when (fee) { @@ -146,17 +141,14 @@ class TransactionManagerImpl( private suspend fun getFeeForEthereumBlockchain( walletManager: EthereumWalletManager, blockchain: Blockchain, - amountToSend: BigDecimal, - currency: Currency, + amountToSend: Amount, destinationAddress: String, data: String?, increaseBy: Int?, ): ProxyFees { val gasLimit = getGasLimit( evmWalletManager = walletManager, - blockchain = blockchain, amount = amountToSend, - currency = currency, destinationAddress = destinationAddress, data = data, ).increaseBigIntegerByPercents(increaseBy) @@ -210,23 +202,20 @@ class TransactionManagerImpl( } } - @Suppress("LongParameterList") private suspend fun getGasLimit( evmWalletManager: EthereumWalletManager, - blockchain: Blockchain, - amount: BigDecimal, - currency: Currency, + amount: Amount, destinationAddress: String, data: String?, ): BigInteger { val result = if (data.isNullOrEmpty()) { evmWalletManager.getGasLimit( - amount = createAmount(amount, currency, blockchain), + amount = amount, destination = destinationAddress, ) } else { evmWalletManager.getGasLimit( - amount = createAmount(amount, currency, blockchain), + amount = amount, destination = destinationAddress, data = data, ) @@ -241,6 +230,18 @@ class TransactionManagerImpl( } } + override suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = getActualWalletManager(blockchain, derivationPath) + val gasPriceResult = (walletManager as? EthereumWalletManager)?.getGasPrice() + ?: error("not supported for $blockchain") + val gasPrice = when (gasPriceResult) { + is Result.Failure -> error("fail to receive gasPrice") + is Result.Success -> gasPriceResult.data + } + return createMultipleProxyFees(gasPrice, gas, blockchain) + } + private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { val selectedUserWallet = requireNotNull( userWalletsListManager.selectedUserWalletSync, @@ -307,27 +308,6 @@ class TransactionManagerImpl( ) } - private fun createAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount { - return when (currency) { - is Currency.NativeToken -> { - Amount(value = amount, blockchain = blockchain) - } - is Currency.NonNativeToken -> { - Amount(convertNonNativeToken(currency), amount) - } - } - } - - private fun convertNonNativeToken(token: Currency.NonNativeToken): Token { - return Token( - name = token.name, - symbol = token.symbol, - contractAddress = token.contractAddress, - decimals = token.decimalCount, - id = token.id, - ) - } - private fun convertToProxyAmount(amount: Amount): ProxyAmount { return ProxyAmount( currencySymbol = amount.currencySymbol, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 7496a0a8e2..5e1eb6d5d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -1,7 +1,6 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json -import java.math.BigDecimal data class ExchangeDataResponseWithTxDetails( val dataResponse: ExchangeDataResponse, @@ -50,7 +49,10 @@ data class TxDetails( val txData: String?, // transaction data if DEX, null if CEX @Json(name = "txValue") - val txValue: BigDecimal, // amount (same as fromAmount) + val txValue: String, // amount (same as fromAmount for Coin, but for bridge equal to otherNativeFee) + + @Json(name = "otherNativeFee") + val otherNativeFee: String?, @Json(name = "externalTxId") val externalTxId: String?, // null if DEX, provider transaction id if CEX @@ -63,6 +65,9 @@ data class TxDetails( @Json(name = "txExtraId") val txExtraId: String?, + + @Json(name = "gas") + val gas: String?, ) enum class TxType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index dc786b02de..7682c70d4f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -7,17 +7,23 @@ data class ExchangeStatusResponse( @Json(name = "providerId") val providerId: String, - @Json(name = "externalTxId") - val externalTxId: String, - @Json(name = "status") val status: ExchangeStatus, + @Json(name = "externalTxId") + val externalTxId: String?, + @Json(name = "externalTxUrl") - val externalTxUrl: String, + val externalTxUrl: String?, @Json(name = "error") val error: ExchangeStatusError?, + + @Json(name = "refundNetwork") + val refundNetwork: String? = null, + + @Json(name = "refundContractAddress") + val refundContractAddress: String? = null, ) enum class ExchangeStatus { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index eb9e2a434c..9fe41bf3a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -18,6 +18,7 @@ interface TangemTechApi { @Query("contractAddress") contractAddress: String? = null, @Query("exchangeable") exchangeable: Boolean? = null, @Query("networkIds") networkIds: String? = null, + @Query("networkId") networkId: String? = null, @Query("active") active: Boolean? = null, @Query("searchText") searchText: String? = null, @Query("offset") offset: Int? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt index 2bcb9a3dbb..cf99b7c4ac 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt @@ -10,9 +10,12 @@ interface SwapTransactionStatusStore { } enum class ExchangeAnalyticsStatus(val value: String) { + WaitingTxHash("Waiting tx hash"), InProgress("In Progress"), Done("Done"), Fail("Fail"), + FailTx("Fail tx"), + Unknown("Unknown"), KYC("KYC"), Refunded("Refunded"), Cancelled("Canceled"), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotification.kt new file mode 100644 index 0000000000..e1b4cc4b24 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotification.kt @@ -0,0 +1,125 @@ +package com.tangem.core.ui.components.notifications + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.colorspace.ColorSpaces +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Currency notification component from Design system. + * + * @param config component config + * @param modifier modifier + * @param containerColor container color + * + * @see Figma component + */ +@Composable +fun CurrencyNotification( + config: CurrencyNotificationConfig, + modifier: Modifier = Modifier, + containerColor: Color? = null, +) { + NotificationBaseContainer( + buttonsState = config.buttonsState, + onClick = null, + onCloseClick = null, + modifier = modifier, + containerColor = containerColor, + ) { + MainContent( + tokenIconState = config.tokenIconState, + title = config.title, + subtitle = config.subtitle, + ) + } +} + +@Composable +private fun MainContent( + tokenIconState: CurrencyIconState, + title: TextReference, + subtitle: CurrencyNotificationConfig.AnnotatedSubtitle, +) { + Row { + CurrencyIcon( + state = tokenIconState, + modifier = Modifier.align(alignment = Alignment.CenterVertically), + ) + + SpacerW(width = TangemTheme.dimens.spacing6) + + TextsBlock(title = title, subtitle = subtitle) + } +} + +@Composable +private fun TextsBlock(title: TextReference, subtitle: CurrencyNotificationConfig.AnnotatedSubtitle) { + Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { + Text( + text = title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + val subtitleValue = subtitle.valueProvider() + ClickableText( + text = subtitleValue, + onClick = { subtitle.onClick(subtitleValue, it) }, + style = TangemTheme.typography.caption2, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_Notification() { + TangemThemePreview { + CurrencyNotification( + config = CurrencyNotificationConfig( + title = resourceReference( + R.string.express_exchange_notification_refund_title, + wrappedList("USDT", "Polygon"), + ), + subtitle = CurrencyNotificationConfig.AnnotatedSubtitle( + valueProvider = { + buildAnnotatedString { + append("Your transaction amount was refunded in USDT to your wallet due to OKX") + } + }, + onClick = { _, _ -> }, + ), + tokenIconState = CurrencyIconState.TokenIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/usd-coin.png", + topBadgeIconResId = R.drawable.ic_polygon_22, + isGrayscale = false, + showCustomBadge = false, + fallbackTint = Color(1.0f, 1.0f, 1.0f, 1.0f, ColorSpaces.Srgb), + fallbackBackground = Color(0.23529412f, 0.28627452f, 0.6117647f, 1.0f, ColorSpaces.Srgb), + ), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Go to token"), + onClick = {}, + ), + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotificationConfig.kt new file mode 100644 index 0000000000..74962546e7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotificationConfig.kt @@ -0,0 +1,35 @@ +package com.tangem.core.ui.components.notifications + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.AnnotatedString +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference + +/** + * Currency notification component state + * + * @property title title + * @property subtitle subtitle + * @property buttonsState buttons state + * @property tokenIconState token icon state + * +[REDACTED_AUTHOR] + */ +data class CurrencyNotificationConfig( + val title: TextReference, + val subtitle: AnnotatedSubtitle, + val tokenIconState: CurrencyIconState, + val buttonsState: NotificationConfig.ButtonsState, +) { + + /** + * Subtitle as [AnnotatedString] + * + * @property valueProvider composable function that provides [AnnotatedString] + * @property onClick lambda be invoked when text in specified position is clicked + */ + data class AnnotatedSubtitle( + val valueProvider: @Composable () -> AnnotatedString, + val onClick: (value: AnnotatedString, position: Int) -> Unit, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index c5b3d10a9b..f25c4014cf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -30,17 +30,19 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState /** * Notification component from Design system. * Use this for Notification with title, subtitle, clickable or not. * - * @param config component config - * @param modifier modifier - * @param iconTint icon tint + * @param config component config + * @param modifier modifier + * @param containerColor container color + * @param iconTint icon tint + * @param isEnabled flag that defines if component is clickable * * @see Figma component @@ -53,44 +55,33 @@ fun Notification( iconTint: Color? = null, isEnabled: Boolean = true, ) { - BaseContainer( + NotificationBaseContainer( buttonsState = config.buttonsState, onClick = config.onClick, + onCloseClick = config.onCloseClick, modifier = modifier, containerColor = containerColor, isEnabled = isEnabled, ) { - Column( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), - ) { - MainContent( - iconResId = config.iconResId, - iconTint = iconTint, - title = config.title, - subtitle = config.subtitle, - isClickableComponent = isEnabled && config.onClick != null, - ) - - Buttons(state = config.buttonsState, isEnabled = isEnabled) - } - - CloseableIconButton( - onClick = config.onCloseClick, - modifier = Modifier.align(alignment = Alignment.TopEnd), - isEnabled = isEnabled, + MainContent( + iconResId = config.iconResId, + iconTint = iconTint, + title = config.title, + subtitle = config.subtitle, + isClickableComponent = isEnabled && config.onClick != null, ) } } @Composable -private fun BaseContainer( +internal fun NotificationBaseContainer( buttonsState: NotificationConfig.ButtonsState?, onClick: (() -> Unit)?, + onCloseClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true, containerColor: Color? = null, - content: @Composable BoxScope.() -> Unit, + content: @Composable ColumnScope.() -> Unit, ) { val tempContainerColor by rememberUpdatedState( newValue = if (buttonsState != null || onClick != null) { @@ -109,7 +100,22 @@ private fun BaseContainer( shape = TangemTheme.shapes.roundedCornersXMedium, color = containerColor ?: tempContainerColor, ) { - Box(content = content) + Box { + Column( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), + ) { + content() + + Buttons(state = buttonsState, isEnabled = isEnabled) + } + + CloseableIconButton( + onClick = onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + isEnabled = isEnabled, + ) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 6b4cc1bd3d..220b5c82ca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -82,7 +82,9 @@ private fun LazyListScope.contentItems( when (item) { is TxHistoryState.TxHistoryItemState.GroupTitle -> item.itemKey is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() - is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash + is TxHistoryState.TxHistoryItemState.Transaction -> + item.state.txHash + + ((item.state as? TransactionState.Content)?.hashCode() ?: "") } }, contentType = txHistoryItems.itemContentType { it::class.java }, diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index e48f0e6933..94a184fb73 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -33,19 +33,17 @@ internal class DefaultPromoRepository( } override suspend fun getOkxPromoBanner(): PromoBanner? { - // TODO disabled for 5.12, enable for 5.12.1 - return null - // return runCatching(dispatchers.io) { - // promoResponseConverter.convert( - // tangemApi.getPromotionInfo(OKX) - // .getOrThrow(), - // ) - // }.getOrNull() + return runCatching(dispatchers.io) { + promoResponseConverter.convert( + tangemApi.getPromotionInfo(OKX) + .getOrThrow(), + ) + }.getOrNull() } private companion object { private const val CHANGELLY_NAME = "changelly" private const val TRAVALA = "travala" - // private const val OKX = "okx" + private const val OKX = "okx" } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index d1a7d70fcf..b24c5226bc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, @@ -53,6 +53,7 @@ internal class DefaultCurrenciesRepository( private val demoConfig = DemoConfig() private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory() + private val cryptoCurrencyFactory = CryptoCurrencyFactory() private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig) private val userTokensResponseFactory = UserTokensResponseFactory() private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() @@ -125,7 +126,7 @@ internal class DefaultCurrenciesRepository( return newTokens .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins .mapNotNull { - CryptoCurrencyFactory().createCoin( + cryptoCurrencyFactory.createCoin( blockchain = getBlockchain(networkId = it.network.id), extraDerivationPath = it.network.derivationPath.value, derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, @@ -415,12 +416,44 @@ internal class DefaultCurrenciesRepository( } override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { - return CryptoCurrencyFactory().createToken( + return cryptoCurrencyFactory.createToken( cryptoCurrency = cryptoCurrency, network = network, ) } + override suspend fun createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token { + val userWallet = getUserWallet(userWalletId) + val token = withContext(dispatchers.io) { + val foundToken = tangemTechApi.getCoins( + contractAddress = contractAddress, + networkId = networkId, + ) + .getOrThrow() + .coins + .firstOrNull() + ?: error("Token not found") + val network = foundToken.networks.firstOrNull { it.networkId == networkId } ?: error("Network not found") + CryptoCurrencyFactory.Token( + symbol = foundToken.symbol, + name = foundToken.name, + contractAddress = contractAddress, + decimals = network.decimalCount?.toInt() ?: error("Decimals not found"), + id = foundToken.id, + ) + } + return cryptoCurrencyFactory.createToken( + token = token, + networkId = networkId, + extraDerivationPath = null, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) ?: error("Unable to create token") + } + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return userTokensStore.get(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index 55b2b0b161..b1e215ed19 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -99,7 +99,7 @@ class CryptoCurrencyFactory { decimals = cryptoCurrency.decimals, id = cryptoCurrency.id.rawCurrencyId, ) - val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.id.value) ?: Blockchain.Unknown + val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index caffe5aca9..56922c5bf0 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -24,7 +24,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber -import java.math.BigDecimal import java.math.BigInteger import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType @@ -41,7 +40,6 @@ internal class DefaultTransactionRepository( destination: String, userWalletId: UserWalletId, network: Network, - isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, ): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) { @@ -58,7 +56,6 @@ internal class DefaultTransactionRepository( memo = memo, destination = destination, network = network, - isSwap = isSwap, txExtras = txExtras, hash = hash, ) @@ -91,7 +88,6 @@ internal class DefaultTransactionRepository( memo = memo, destination = destination, network = network, - isSwap = isSwap, txExtras = txExtras, hash = hash, ) @@ -158,23 +154,15 @@ internal class DefaultTransactionRepository( memo: String?, destination: String, network: Network, - isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, ): TransactionData.Uncompiled { - // TODO: refactor workaround to use general mechanism in bsdk for build tx for DEX - val txAmount = if (isSwap) { - createAmountForSwap(amount) - } else { - amount - } - if (txExtras != null && memo != null) { // throw error for now to avoid programmers errors when use extras error("Both txExtras and memo provided, use only one of them") } val extras = txExtras ?: getMemoExtras(network.id.value, memo) - return createTransaction(txAmount, fee, destination).copy( + return createTransaction(amount, fee, destination).copy( hash = hash, extras = extras, ) @@ -203,19 +191,4 @@ internal class DefaultTransactionRepository( else -> null } } - - private fun createAmountForSwap(amount: Amount): Amount { - return when (amount.type) { - is AmountType.Coin -> amount - else -> { - // 1. when creates swap amount for NonNativeToken, amount should be ZERO - // 2. Amount has .Coin type, as workaround to use destinationAddress in bsdk, not contractAddress - Amount( - currencySymbol = amount.currencySymbol, - value = BigDecimal.ZERO, - decimals = amount.decimals, - ) - } - } - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index e8c6f0b7a8..01223c61a3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -79,13 +79,35 @@ class AddCryptoCurrenciesUseCase( .toNonEmptyListOrNull() ?: return@either - catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) { - raise(it) - } - + addCurrencies(userWalletId, currenciesToAdd) refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies) } + suspend operator fun invoke( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): Either = either { + val existingCurrencies = + catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) { + raise(it) + } + val foundToken = existingCurrencies + .filterIsInstance() + .firstOrNull { + it.network.backendId == networkId && + !it.isCustom && + it.contractAddress.equals(contractAddress, true) + } + if (foundToken != null) { + return@either foundToken + } + val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) + addCurrencies(userWalletId, listOf(tokenToAdd)) + refreshUpdatedNetworks(userWalletId, listOf(tokenToAdd), existingCurrencies) + tokenToAdd + } + /** * Refreshes the network statuses for tokens that have corresponding coins in the * [existingCurrencies] list. @@ -117,6 +139,33 @@ class AddCryptoCurrenciesUseCase( } } + private suspend fun Raise.createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token { + return catch( + block = { + currenciesRepository.createTokenCurrency( + userWalletId = userWalletId, + contractAddress = contractAddress, + networkId = networkId, + ) + }, + catch = { + raise(it) + }, + ) + } + + private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, tokens: List) { + catch( + { currenciesRepository.addCurrencies(userWalletId, tokens) }, + ) { + raise(it) + } + } + /** * Determines if the [existingCurrencies] list contains a coin that corresponds * to the given [token]. diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 782c16153e..4d8d4f0522 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.Flow /** * Repository for everything related to the tokens of user wallet * */ +@Suppress("TooManyFunctions") interface CurrenciesRepository { /** @@ -213,4 +214,13 @@ interface CurrenciesRepository { * Creates token [cryptoCurrency] based on current token and [network] it`s will be added */ fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token + + /** + * Creates token [cryptoCurrency] based on [contractAddress] and [networkId] it`s will be added + */ + suspend fun createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index e81b9e83d1..c1c3aa5e5a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -136,4 +136,12 @@ internal class MockCurrenciesRepository( override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { return cryptoCurrency } + + override suspend fun createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token { + error("not implemented") + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 2228ec19e5..3fb7cbf0b5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -18,7 +18,6 @@ interface TransactionRepository { destination: String, userWalletId: UserWalletId, network: Network, - isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, ): TransactionData.Uncompiled? diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index 056dfc0f0c..ab5172eaf8 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -24,7 +24,6 @@ class CreateTransactionUseCase( userWalletId: UserWalletId, network: Network, txExtras: TransactionExtras? = null, - isSwap: Boolean = false, hash: String? = null, ) = Either.catch { requireNotNull( @@ -35,7 +34,6 @@ class CreateTransactionUseCase( destination = destination, userWalletId = userWalletId, network = network, - isSwap = isSwap, txExtras = txExtras, hash = hash, ), diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt index 0a566d5500..d782b606f9 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt @@ -15,6 +15,8 @@ internal class ExchangeStatusConverter : Converter { @@ -24,19 +25,30 @@ internal class ExpressDataConverter : Converter = emptyList(), + val swapProvider: SwapProvider, ) : SwapState data class EmptyAmountState(val zeroAmountEquivalent: String) : SwapState @@ -102,6 +107,9 @@ data class TxFee( val gasLimit: Int, val feeFiatFormatted: String, val feeCryptoFormatted: String, + val feeIncludeOtherNativeFee: BigDecimal, + val feeFiatFormattedWithNative: String, + val feeCryptoFormattedWithNative: String, val decimals: Int, val cryptoSymbol: String, val feeType: FeeType, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 18679abdb6..3f3656b18c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal @@ -229,7 +230,6 @@ internal class SwapInteractorImpl @Inject constructor( permissionOptions.fromToken.network, permissionOptions.txFee.gasLimit, ), - isSwap = false, ).getOrElse { Timber.e(it, "Failed to create approveTransaction") return SwapTransactionState.UnknownError @@ -325,8 +325,8 @@ internal class SwapInteractorImpl @Inject constructor( val fromTokenAddress = getTokenAddress(fromToken.currency) val isAllowedToSpend = quotes.fold( - ifRight = { - it.allowanceContract?.let { + ifRight = { quotes -> + quotes.allowanceContract?.let { isAllowedToSpend(networkId, fromToken.currency, amount, it) } ?: true }, @@ -351,7 +351,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } else { provider to getQuotesState( - exchangeProviderType = provider.type, + provider = provider, quoteDataModel = quotes, amount = amount, fromToken = fromToken, @@ -375,7 +375,6 @@ internal class SwapInteractorImpl @Inject constructor( isBalanceWithoutFeeEnough: Boolean, ): Pair { return provider to loadCexQuoteData( - exchangeProviderType = ExchangeProviderType.CEX, networkId = networkId, amount = amount, fromTokenStatus = fromToken, @@ -602,8 +601,8 @@ internal class SwapInteractorImpl @Inject constructor( swapData = requireNotNull(swapData), currencyToSendStatus = currencyToSend, currencyToGetStatus = currencyToGet, - amountToSwap = amountToSwap, fee = fee, + amountToSwap = amountToSwap, userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } @@ -629,8 +628,8 @@ internal class SwapInteractorImpl @Inject constructor( ) val fee = when (val txFee = state.txFee) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue - is TxFeeState.SingleFeeState -> txFee.fee.feeValue + is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee + is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee } val feeState = getFeeState( fee = fee, @@ -663,8 +662,9 @@ internal class SwapInteractorImpl @Inject constructor( val derivationPath = currencyToSendStatus.currency.network.derivationPath.value val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX val dataToSign = dexTransaction.txData + val amountToSend = createNativeAmountForDex(swapData.transaction.txValue, currencyToSendStatus.currency.network) val txData = createTransactionUseCase( - amount = amount.value.convertToAmount(currencyToSendStatus.currency), + amount = amountToSend, fee = getFeeForTransaction( fee = fee, blockchain = Blockchain.fromId(currencyToSendStatus.currency.network.id.value), @@ -675,7 +675,6 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSendStatus.currency.network, txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, fee.gasLimit), hash = dataToSign, - isSwap = true, ).getOrElse { Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.UnknownError @@ -877,7 +876,7 @@ internal class SwapInteractorImpl @Inject constructor( ) blockchain == Blockchain.Aptos -> { val gasUnitPrice = fee.feeValue.divide( - BigDecimal(fee.gasLimit), + fee.gasLimit.toBigDecimal(), Blockchain.Aptos.decimals(), RoundingMode.HALF_UP, ) @@ -991,7 +990,6 @@ internal class SwapInteractorImpl @Inject constructor( */ @Suppress("LongParameterList") private suspend fun loadCexQuoteData( - exchangeProviderType: ExchangeProviderType, networkId: String, amount: SwapAmount, fromTokenStatus: CryptoCurrencyStatus, @@ -1042,7 +1040,7 @@ internal class SwapInteractorImpl @Inject constructor( ) getQuotesState( - exchangeProviderType = exchangeProviderType, + provider = provider, quoteDataModel = quotes, amount = amount, fromToken = fromTokenStatus, @@ -1059,7 +1057,7 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongMethod") private suspend fun getQuotesState( - exchangeProviderType: ExchangeProviderType, + provider: SwapProvider, quoteDataModel: Either, amount: SwapAmount, fromToken: CryptoCurrencyStatus, @@ -1081,6 +1079,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = quoteModel.toTokenAmount, swapData = null, txFeeState = txFee, + provider = provider, ).copy( warnings = manageWarnings( fromTokenStatus = fromToken, @@ -1090,7 +1089,7 @@ internal class SwapInteractorImpl @Inject constructor( ), ) - when (exchangeProviderType) { + when (provider.type) { ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { val state = updatePermissionState( networkId = networkId, @@ -1154,8 +1153,8 @@ internal class SwapInteractorImpl @Inject constructor( ): IncludeFeeInAmount { val feeValue = when (txFee) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue - is TxFeeState.SingleFeeState -> txFee.fee.feeValue + is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee + is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee } val feePaidCurrency = getFeePaidCurrency( userWalletId = requireNotNull(getSelectedWallet()).walletId, @@ -1261,35 +1260,43 @@ internal class SwapInteractorImpl @Inject constructor( providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(), + refundAddress = fromToken.value.networkAddress?.defaultAddress?.value, ).fold( ifRight = { swapData -> + val transaction = swapData.transaction as ExpressTransactionModel.DEX + val nativeCoinDecimals = Blockchain.fromNetworkId(networkId)?.decimals() + ?: error("Blockchain not found") + val otherNativeFee = transaction.otherNativeFeeWei + ?.movePointLeft(nativeCoinDecimals) + ?: BigDecimal.ZERO val userWallet = getSelectedWallet() - val cardId = userWallet?.scanResponse?.card?.cardId - val feeData = if (cardId != null && isDemoCardUseCase(cardId)) { - getDemoFees(fromToken.currency) - } else { - transactionManager.getFee( + val txFeeState = when ( + val feeData = getFeeDataForDexSwap( networkId = networkId, - amountToSend = amount.value, - currencyToSend = swapCurrencyConverter.convert(fromToken.currency), - destinationAddress = swapData.transaction.txTo, - increaseBy = INCREASE_GAS_LIMIT_BY, - data = (swapData.transaction as ExpressTransactionModel.DEX).txData, - derivationPath = fromToken.currency.network.derivationPath.value, + transaction = transaction, + fromToken = fromToken.currency, + cardId = userWallet?.scanResponse?.card?.cardId, ) - } - val txFeeState = when (feeData) { - is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency) - is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency) + ) { + is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) + is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) } val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) - val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeByPriority) + val feeToCheckFunds = feeByPriority + (otherNativeFee ?: BigDecimal.ZERO) + val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeToCheckFunds) val feeState = getFeeState( - fee = feeByPriority, + fee = feeToCheckFunds, spendAmount = amount, networkId = networkId, fromTokenStatus = fromToken, ) + val preparedSwapConfigState = PreparedSwapConfigState( + isAllowedToSpend = true, + isBalanceEnough = isBalanceIncludeFeeEnough, + feeState = feeState, + hasOutgoingTransaction = hasOutgoingTransaction(fromToken), + includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex + ) val swapState = updateBalances( networkId = networkId, fromTokenStatus = fromToken, @@ -1298,6 +1305,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = swapData.toTokenAmount, swapData = swapData, txFeeState = txFeeState, + provider = provider, ) swapState.copy( permissionState = PermissionDataState.Empty, @@ -1305,17 +1313,9 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus = fromToken, amount = amount, feeState = txFeeState, - minAdaValue = (feeData as? ProxyFees.SingleFee)?.let { - (it.singleFee as? ProxyFee.CardanoToken)?.minAdaValue - }, - ), - preparedSwapConfigState = PreparedSwapConfigState( - isAllowedToSpend = true, - isBalanceEnough = isBalanceIncludeFeeEnough, - feeState = feeState, - hasOutgoingTransaction = hasOutgoingTransaction(fromToken), - includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex + minAdaValue = null, // no ADA in DEX ), + preparedSwapConfigState = preparedSwapConfigState, ) }, ifLeft = { error -> @@ -1335,8 +1335,46 @@ internal class SwapInteractorImpl @Inject constructor( ) } + private suspend fun getFeeDataForDexSwap( + networkId: String, + transaction: ExpressTransactionModel.DEX, + fromToken: CryptoCurrency, + cardId: String?, + ): ProxyFees { + if (cardId != null && isDemoCardUseCase(cardId)) { + return getDemoFees(fromToken) + } + return try { + val nativeBalance = userWalletManager.getNativeTokenBalance( + networkId = networkId, + derivationPath = fromToken.network.derivationPath.value, + ) ?: ProxyAmount.empty() + val amountToSend = createNativeAmountForDex(transaction.txValue, fromToken.network) + // transaction.txValue is always native coin + if (nativeBalance.value < amountToSend.value) { + error("It's impossible to calculate fee for nativeBalance.value < amountToSend.value") + } + transactionManager.getFee( + networkId = networkId, + amountToSend = amountToSend, + currencyToSend = swapCurrencyConverter.convert(fromToken), + destinationAddress = transaction.txTo, + increaseBy = INCREASE_GAS_LIMIT_BY, + data = transaction.txData, + derivationPath = fromToken.network.derivationPath.value, + ) + } catch (e: IllegalStateException) { + transactionManager.getFeeForGas( + networkId = networkId, + gas = transaction.gas.multiply(INCREASE_GAS_LIMIT_BY.toBigInteger()).divide(100.toBigInteger()), + derivationPath = fromToken.network.derivationPath.value, + ) + } + } + @Suppress("LongParameterList") private suspend fun updateBalances( + provider: SwapProvider, networkId: String, fromTokenStatus: CryptoCurrencyStatus, toTokenStatus: CryptoCurrencyStatus, @@ -1348,7 +1386,6 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency val nativeToken = repository.getNativeTokenForNetwork(networkId) - val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( @@ -1371,6 +1408,7 @@ internal class SwapInteractorImpl @Inject constructor( ), swapDataModel = swapData, txFee = txFeeState, + swapProvider = provider, ) } @@ -1380,7 +1418,7 @@ internal class SwapInteractorImpl @Inject constructor( ): TxFeeState { return txFeeResult?.fold( ifLeft = { TxFeeState.Empty }, - ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency) }, + ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency, null) }, ) ?: TxFeeState.Empty } @@ -1440,7 +1478,7 @@ internal class SwapInteractorImpl @Inject constructor( try { transactionManager.getFee( networkId = networkId, - amountToSend = BigDecimal.ZERO, + amountToSend = createNativeAmountForDex("0", fromToken.network), currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), destinationAddress = fromToken.getContractAddress(), increaseBy = INCREASE_GAS_LIMIT_BY, @@ -1488,11 +1526,16 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState { + private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState( + fromToken: CryptoCurrency, + otherNativeFee: BigDecimal? = null, + ): TxFeeState { + val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee val normalFeeGas = this.minFee.gasLimit.toInt() val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee val priorityFeeGas = this.normalFee.gasLimit.toInt() + // region fees to use val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue, priorityFeeValue) val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" } @@ -1504,12 +1547,35 @@ internal class SwapInteractorImpl @Inject constructor( amount = priorityFeeValue, decimals = normalFee.fee.decimals, ) + // endregion + // region fees include otherNativeFee + val feesFiatWithNative = getFormattedFiatFees( + fromToken = fromToken, + normalFeeValue + otherNativeFeeValue, + priorityFeeValue + otherNativeFeeValue, + ) + val normalFiatFeeWithNative = + requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" } + val priorityFiatFeeWithNative = + requireNotNull(feesFiatWithNative.getOrNull(1)) { "feesFiat item 1 couldn't be null" } + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeValue + otherNativeFeeValue, + decimals = minFee.fee.decimals, + ) + val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = priorityFeeValue + otherNativeFeeValue, + decimals = normalFee.fee.decimals, + ) + // endregion return TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = normalFeeValue, gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue, + feeFiatFormattedWithNative = normalFiatFeeWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = minFee.fee.decimals, cryptoSymbol = minFee.fee.currencySymbol, feeType = FeeType.NORMAL, @@ -1519,6 +1585,9 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = priorityFeeGas, feeFiatFormatted = priorityFiatFee, feeCryptoFormatted = priorityCryptoFee, + feeIncludeOtherNativeFee = priorityFeeValue + otherNativeFeeValue, + feeFiatFormattedWithNative = priorityFiatFeeWithNative, + feeCryptoFormattedWithNative = priorityCryptoFeeWithNative, decimals = normalFee.fee.decimals, cryptoSymbol = normalFee.fee.currencySymbol, feeType = FeeType.PRIORITY, @@ -1526,7 +1595,11 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState { + private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState( + fromToken: CryptoCurrency, + otherNativeFee: BigDecimal? = null, + ): TxFeeState { + val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO val normalFeeValue = this.singleFee.fee.value val normalFeeGas = this.singleFee.gasLimit.toInt() val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue) @@ -1535,12 +1608,28 @@ internal class SwapInteractorImpl @Inject constructor( amount = normalFeeValue, decimals = singleFee.fee.decimals, ) + // region fees include otherNativeFee + val feesFiatWithNative = getFormattedFiatFees( + fromToken = fromToken, + normalFeeValue + otherNativeFeeValue, + normalFeeValue + otherNativeFeeValue, + ) + val normalFiatFeeWithNative = + requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" } + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeValue + otherNativeFeeValue, + decimals = singleFee.fee.decimals, + ) + // endregion return TxFeeState.SingleFeeState( fee = TxFee( feeValue = normalFeeValue, gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue, + feeFiatFormattedWithNative = normalFiatFeeWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = singleFee.fee.decimals, cryptoSymbol = singleFee.fee.currencySymbol, feeType = FeeType.NORMAL, @@ -1548,7 +1637,12 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun TransactionFee.toTxFeeState(fromToken: CryptoCurrency): TxFeeState { + @Suppress("LongMethod") + private suspend fun TransactionFee.toTxFeeState( + fromToken: CryptoCurrency, + otherNativeFee: BigDecimal?, + ): TxFeeState { + val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO return when (this) { is TransactionFee.Choosable -> { val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND) @@ -1566,12 +1660,31 @@ internal class SwapInteractorImpl @Inject constructor( amount = feePriority, decimals = priorityFee.amount.decimals, ) + + // region otherNativeFee + val normalFeeWithOtherNative = feeNormal + otherNativeFeeValue + val priorityFeeWithOtherNative = feePriority + otherNativeFeeValue + val normalFiatValueWithNative = getFormattedFiatFees(fromToken, normalFeeWithOtherNative)[0] + val priorityFiatValueWithNative = getFormattedFiatFees(fromToken, priorityFeeWithOtherNative)[0] + + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeWithOtherNative, + decimals = normalFee.amount.decimals, + ) + val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = priorityFeeWithOtherNative, + decimals = priorityFee.amount.decimals, + ) + // endregion TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = feeNormal, gasLimit = normalFee.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeWithOtherNative, + feeFiatFormattedWithNative = normalFiatValueWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = normalFee.amount.decimals, cryptoSymbol = normalFee.amount.currencySymbol, feeType = FeeType.NORMAL, @@ -1581,6 +1694,9 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = priorityFee.getGasLimit(), feeFiatFormatted = priorityFiatValue, feeCryptoFormatted = priorityCryptoFee, + feeIncludeOtherNativeFee = priorityFeeWithOtherNative, + feeFiatFormattedWithNative = priorityFiatValueWithNative, + feeCryptoFormattedWithNative = priorityCryptoFeeWithNative, decimals = priorityFee.amount.decimals, cryptoSymbol = priorityFee.amount.currencySymbol, feeType = FeeType.PRIORITY, @@ -1594,12 +1710,24 @@ internal class SwapInteractorImpl @Inject constructor( amount = feeNormal, decimals = this.normal.amount.decimals, ) + // region otherNativeFee + val normalFeeWithOtherNative = feeNormal + otherNativeFeeValue + val normalFiatValueWithNative = getFormattedFiatFees(fromToken, normalFeeWithOtherNative)[0] + + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeWithOtherNative, + decimals = this.normal.amount.decimals, + ) + // endregion TxFeeState.SingleFeeState( fee = TxFee( feeValue = this.normal.amount.value ?: BigDecimal.ZERO, gasLimit = this.normal.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeWithOtherNative, + feeFiatFormattedWithNative = normalFiatValueWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = normal.amount.decimals, cryptoSymbol = normal.amount.currencySymbol, feeType = FeeType.NORMAL, @@ -1609,6 +1737,18 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun createNativeAmountForDex(txValueAmount: String, network: Network): Amount { + val nativeDecimals = Blockchain.fromNetworkId(network.backendId)?.decimals() + ?: error("Blockchain not found") + val decimalValue = txValueAmount.toBigDecimalOrNull()?.movePointLeft(nativeDecimals) + ?: error("txValue parse error") + return Amount( + currencySymbol = network.currencySymbol, + value = decimalValue, + decimals = nativeDecimals, + ) + } + /** * Workaround to increase gas limit cause we calculate fee for random address */ @@ -1669,7 +1809,7 @@ internal class SwapInteractorImpl @Inject constructor( if (fromToken.currency is CryptoCurrency.Token) { tokenBalance >= amount.value } else { - tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO) + tokenBalance >= amount.value.plus(fee ?: BigDecimal.ZERO) } } } @@ -1856,7 +1996,6 @@ internal class SwapInteractorImpl @Inject constructor( normalFee = ProxyFee.Common( gasLimit = 1.toBigInteger(), fee = demoFee.copy(value = normalDemoFee), - ), priorityFee = ProxyFee.Common( gasLimit = 1.toBigInteger(), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index f901691c85..003f258f6f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.core.ui.R @@ -75,18 +74,21 @@ data class SwapButton( sealed interface TransactionCardType { - val headerResId: Int + val header: TextReference + val isError: Boolean data class Inputtable( val onAmountChanged: ((String) -> Unit), val onFocusChanged: ((Boolean) -> Unit), - @StringRes override val headerResId: Int = R.string.swapping_from_title, + override val isError: Boolean, + override val header: TextReference = TextReference.Res(R.string.swapping_from_title), ) : TransactionCardType data class ReadOnly( val showWarning: Boolean = false, val onWarningClick: (() -> Unit)? = null, - @StringRes override val headerResId: Int = R.string.swapping_to_title, + override val isError: Boolean = false, + override val header: TextReference = TextReference.Res(R.string.swapping_to_title), ) : TransactionCardType } @@ -103,7 +105,7 @@ data class LegalState( sealed interface SwapWarning { data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning - object InsufficientFunds : SwapWarning + data object InsufficientFunds : SwapWarning data class NoAvailableTokensToSwap(val notificationConfig: NotificationConfig) : SwapWarning data class GenericWarning( val title: TextReference? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c9ad194413..517c795db6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -54,7 +54,11 @@ internal class StateBuilder( return SwapStateHolder( blockchainId = networkInfo.blockchainId, sendCardData = SwapCardState.SwapCardData( - type = TransactionCardType.Inputtable(actions.onAmountChanged, actions.onAmountSelected), + type = TransactionCardType.Inputtable( + onAmountChanged = actions.onAmountChanged, + onFocusChanged = actions.onAmountSelected, + isError = false, + ), amountEquivalent = null, amountTextFieldValue = null, token = null, @@ -155,9 +159,13 @@ internal class StateBuilder( val canSelectReceiveToken = mainTokenId != toToken.id.value if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder + val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + type = sendInput, amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = null, token = uiStateHolder.sendCardData.token, @@ -225,9 +233,19 @@ internal class StateBuilder( val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus + val isInsufficientFunds = isInsufficientFundsCondition(quoteModel) + val insufficientFundsHeader = if (isInsufficientFunds) { + TextReference.Res(R.string.swapping_insufficient_funds) + } else { + TextReference.Res(R.string.swapping_from_title) + } + val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( + isError = isInsufficientFunds, + header = insufficientFundsHeader, + ) return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + type = sendInput, amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat), token = fromCurrencyStatus, @@ -486,7 +504,7 @@ internal class StateBuilder( ) { warnings.add( SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(providerName, fromToken.symbol), + createPermissionNotificationConfig(fromToken.symbol, providerName), ), ) } @@ -504,8 +522,8 @@ internal class StateBuilder( warnings.add( SwapWarning.GeneralWarning( createNetworkFeeCoverageNotificationConfig( - fee.feeCryptoFormatted, - fee.feeFiatFormatted, + fee.feeCryptoFormattedWithNative, + fee.feeFiatFormattedWithNative, ), ), ) @@ -558,13 +576,16 @@ internal class StateBuilder( warnings: MutableList, ) { // check isBalanceEnough, but for dex includeFeeInAmount always Excluded - if (!quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included - ) { + if (isInsufficientFundsCondition(quoteModel)) { warnings.add(SwapWarning.InsufficientFunds) } } + private fun isInsufficientFundsCondition(quoteModel: SwapState.QuotesLoadedState): Boolean { + return !quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included + } + private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean { val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value if (status is CryptoCurrencyStatus.NoAccount) { @@ -975,9 +996,9 @@ internal class StateBuilder( return FeeItemState.Content( feeType = feeType, title = resourceReference(R.string.common_network_fee_title), - amountCrypto = fee.feeCryptoFormatted, + amountCrypto = fee.feeCryptoFormattedWithNative, // display fee with native as workaround for okx symbolCrypto = fee.cryptoSymbol, - amountFiatFormatted = fee.feeFiatFormatted, + amountFiatFormatted = fee.feeFiatFormattedWithNative, // display fee with native as workaround for okx isClickable = isClickable, onClick = actions.onClickFee, ) @@ -1017,8 +1038,7 @@ internal class StateBuilder( val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount)) - val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName || - providerState.type == ExchangeProviderType.DEX_BRIDGE.providerName + val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName return uiState.copy( successState = SwapSuccessStateHolder( timestamp = swapTransactionState.timestamp, @@ -1028,7 +1048,7 @@ internal class StateBuilder( showStatusButton = shouldShowStatus, providerIcon = providerState.iconUrl, rate = providerState.subtitle, - fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), + fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"), fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()), fromTokenFiatAmount = stringReference(fromFiatAmount), @@ -1375,18 +1395,18 @@ internal class StateBuilder( FeeItemState.Content( feeType = this.normalFee.feeType, title = resourceReference(R.string.common_network_fee_title), - amountCrypto = this.normalFee.feeCryptoFormatted, + amountCrypto = this.normalFee.feeCryptoFormattedWithNative, symbolCrypto = this.normalFee.cryptoSymbol, - amountFiatFormatted = this.normalFee.feeFiatFormatted, + amountFiatFormatted = this.normalFee.feeFiatFormattedWithNative, isClickable = true, onClick = {}, ), FeeItemState.Content( feeType = this.priorityFee.feeType, title = resourceReference(R.string.common_network_fee_title), - amountCrypto = this.priorityFee.feeCryptoFormatted, + amountCrypto = this.priorityFee.feeCryptoFormattedWithNative, symbolCrypto = this.priorityFee.cryptoSymbol, - amountFiatFormatted = this.priorityFee.feeFiatFormatted, + amountFiatFormatted = this.priorityFee.feeFiatFormattedWithNative, isClickable = true, onClick = {}, ), @@ -1419,7 +1439,7 @@ internal class StateBuilder( } // region warnings - private fun createPermissionNotificationConfig(providerName: String, fromTokenSymbol: String): NotificationConfig { + private fun createPermissionNotificationConfig(fromTokenSymbol: String, providerName: String): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.express_provider_permission_needed), subtitle = resourceReference( @@ -1621,7 +1641,7 @@ internal class StateBuilder( id = this.providerId, name = this.name, iconUrl = this.imageLarge, - type = this.type.toString(), + type = this.type.providerName, selectionType = selectionType, alertText = alertText, onProviderClick = onProviderClick, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 4dd7a9395f..f3004d1c73 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -435,7 +435,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U // region preview private val sendCard = SwapCardState.SwapCardData( - type = TransactionCardType.Inputtable({}, {}), + type = TransactionCardType.Inputtable({}, {}, false), amountTextFieldValue = TextFieldValue(), amountEquivalent = "1 000 000", tokenIconUrl = "", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 7ae3edc6a2..9baf09e90a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -37,6 +37,7 @@ import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.ImageBackgroundContrastChecker @@ -185,10 +186,14 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - val title = type.headerResId + val titleColor = if (type.isError) { + TangemTheme.colors.text.warning + } else { + TangemTheme.colors.text.tertiary + } Text( - text = stringResource(id = title), - color = TangemTheme.colors.text.tertiary, + text = type.header.resolveReference(), + color = titleColor, maxLines = 1, style = MaterialTheme.typography.subtitle2, modifier = Modifier @@ -546,7 +551,7 @@ private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() { @Composable private fun TransactionCardPreview() { TransactionCard( - type = TransactionCardType.Inputtable({}, {}), + type = TransactionCardType.Inputtable({}, {}, false), amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 52f0679e6c..26daa6490f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -29,6 +29,7 @@ internal data class SwapTransactionsState( val fromFiatAmount: String, val fromCurrencyIcon: CurrencyIconState, val showProviderLink: Boolean, + val isRefundTerminalStatus: Boolean = true, val onClick: () -> Unit, val onGoToProviderClick: (String) -> Unit, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt index ac825983b8..47508d351e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -1,38 +1,92 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.components import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.notifications.CurrencyNotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.tokendetails.impl.R @Immutable -internal sealed class ExchangeStatusNotifications(val config: NotificationConfig) { +internal sealed interface ExchangeStatusNotifications { - data class NeedVerification( - val onGoToProviderClick: () -> Unit, - ) : ExchangeStatusNotifications( + sealed class CommonNotification(val config: NotificationConfig) : ExchangeStatusNotifications + + data class NeedVerification(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( - title = TextReference.Res(R.string.express_exchange_notification_verification_title), - subtitle = TextReference.Res(R.string.express_exchange_notification_verification_text), + title = resourceReference(R.string.express_exchange_notification_verification_title), + subtitle = resourceReference(R.string.express_exchange_notification_verification_text), iconResId = R.drawable.ic_alert_triangle_20, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = TextReference.Res(R.string.common_go_to_provider), + text = resourceReference(R.string.common_go_to_provider), onClick = onGoToProviderClick, ), ), ) - data class Failed( - val onGoToProviderClick: () -> Unit, - ) : ExchangeStatusNotifications( + data class Failed(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( - title = TextReference.Res(R.string.express_exchange_notification_failed_title), - subtitle = TextReference.Res(R.string.express_exchange_notification_failed_text), + title = resourceReference(R.string.express_exchange_notification_failed_title), + subtitle = resourceReference(R.string.express_exchange_notification_failed_text), iconResId = R.drawable.ic_alert_circle_24, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = TextReference.Res(R.string.common_go_to_provider), + text = resourceReference(R.string.common_go_to_provider), onClick = onGoToProviderClick, ), ), ) + + data class TokenRefunded( + val cryptoCurrency: CryptoCurrency, + val onReadMoreClick: () -> Unit, + val onGoToTokenClick: () -> Unit, + ) : ExchangeStatusNotifications { + + val config = CurrencyNotificationConfig( + title = resourceReference( + id = R.string.express_exchange_notification_refund_title, + formatArgs = wrappedList(cryptoCurrency.symbol, cryptoCurrency.network.name), + ), + subtitle = CurrencyNotificationConfig.AnnotatedSubtitle( + valueProvider = { + val linkText = stringResource(R.string.common_read_more) + val fullString = stringResource( + R.string.express_exchange_notification_refund_text, + cryptoCurrency.symbol, + linkText, + ) + + val linkTextPosition = fullString.length - linkText.length + + buildAnnotatedString { + withStyle(SpanStyle(TangemTheme.colors.text.tertiary)) { + append(fullString.substring(0, linkTextPosition)) + } + + withStyle(SpanStyle(TangemTheme.colors.text.accent)) { + append(fullString.substring(linkTextPosition, fullString.length)) + } + } + }, + onClick = { value, position -> + val readMoreStyle = requireNotNull(value.spanStyles.getOrNull(1)) + if (position in readMoreStyle.start..readMoreStyle.end) { + onReadMoreClick() + } + }, + ), + tokenIconState = CryptoCurrencyToIconStateConverter().convert(cryptoCurrency), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_go_to_token), + onClick = onGoToTokenClick, + ), + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index a0231a352a..15523c353c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -25,6 +25,7 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal +import java.util.Locale internal class TokenDetailsSwapTransactionsStateConverter( private val clickIntents: TokenDetailsClickIntents, @@ -62,7 +63,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( }?.fiatRate?.multiply(fromAmount) val timestamp = transaction.timestamp val notifications = - getNotification(transaction.status?.status, transaction.status?.txExternalUrl) + getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null) val showProviderLink = getShowProviderLink(notifications, transaction.status) result.add( SwapTransactionsState( @@ -77,10 +78,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( statuses = getStatuses(transaction.status?.status), hasFailed = transaction.status?.status == ExchangeStatus.Failed, activeStatus = transaction.status?.status, - notification = getNotification( - transaction.status?.status, - transaction.status?.txExternalUrl, - ), + notification = notifications, toCryptoCurrency = toCryptoCurrency, toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( cryptoAmount = toAmount, @@ -110,10 +108,15 @@ internal class TokenDetailsSwapTransactionsStateConverter( return result.toPersistentList() } - fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?): SwapTransactionsState { + fun updateTxStatus( + tx: SwapTransactionsState, + statusModel: ExchangeStatusModel?, + refundToken: CryptoCurrency?, + isRefundTerminalStatus: Boolean, + ): SwapTransactionsState { if (statusModel == null || tx.activeStatus == statusModel.status) return tx val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed - val notifications = getNotification(statusModel.status, statusModel.txExternalUrl) + val notifications = getNotification(statusModel.status, statusModel.txExternalUrl, refundToken) val showProviderLink = getShowProviderLink(notifications, statusModel) return tx.copy( activeStatus = statusModel.status, @@ -122,6 +125,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( statuses = getStatuses(statusModel.status, hasFailed), txUrl = statusModel.txExternalUrl, showProviderLink = showProviderLink, + isRefundTerminalStatus = isRefundTerminalStatus, ) } @@ -133,10 +137,14 @@ internal class TokenDetailsSwapTransactionsStateConverter( ) } - private fun getNotification(status: ExchangeStatus?, txUrl: String?): ExchangeStatusNotifications? { - if (txUrl == null) return null + private fun getNotification( + status: ExchangeStatus?, + txUrl: String?, + refundToken: CryptoCurrency?, + ): ExchangeStatusNotifications? { return when (status) { ExchangeStatus.Failed -> { + if (txUrl == null) return null ExchangeStatusNotifications.Failed { analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderFail(cryptoCurrency.symbol), @@ -145,6 +153,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } ExchangeStatus.Verifying -> { + if (txUrl == null) return null ExchangeStatusNotifications.NeedVerification { analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderKYC(cryptoCurrency.symbol), @@ -152,6 +161,17 @@ internal class TokenDetailsSwapTransactionsStateConverter( clickIntents.onGoToProviderClick(txUrl) } } + ExchangeStatus.Refunded -> { + if (refundToken == null) { + null + } else { + ExchangeStatusNotifications.TokenRefunded( + cryptoCurrency = refundToken, + onReadMoreClick = { clickIntents.onOpenUrlClick(url = getAboutCrossChainBridgesLink()) }, + onGoToTokenClick = { clickIntents.onGoToRefundedTokenClick(refundToken) }, + ) + } + } else -> null } } @@ -287,7 +307,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( status = ExchangeStatus.Refunded, text = TextReference.Res(R.string.express_exchange_status_refunded), isActive = false, - isDone = isRefunded, + isDone = false, ) else -> ExchangeStatusState( status = ExchangeStatus.Sending, @@ -300,4 +320,12 @@ internal class TokenDetailsSwapTransactionsStateConverter( isDone = isSendingDone, ) } + + private fun getAboutCrossChainBridgesLink(): String { + return if (Locale.getDefault().country == "RU") { + "https://tangem.com/ru/blog/post/an-overview-of-cross-chain-bridges/" + } else { + "https://tangem.com/en/blog/post/an-overview-of-cross-chain-bridges/" + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index c715a6c6bd..ff2dd3baff 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -115,6 +115,11 @@ private fun ExchangeStatusStep( color = TangemTheme.colors.icon.warning, isDone = it.isDone, ) + it.status == ExchangeStatus.Refunded -> ExchangeStep( + iconRes = R.drawable.ic_close_24, + color = TangemTheme.colors.icon.warning, + isDone = it.isDone, + ) it.status == ExchangeStatus.Verifying -> ExchangeStep( iconRes = R.drawable.ic_exclamation_24, color = TangemTheme.colors.icon.attention, @@ -141,6 +146,7 @@ private fun ExchangeStatusStep( private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { val textColor = when { stepStatus.status == ExchangeStatus.Cancelled -> TangemTheme.colors.icon.warning + stepStatus.status == ExchangeStatus.Refunded -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Failed && !stepStatus.isDone -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention stepStatus.isDone -> TangemTheme.colors.text.primary1 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index a1a7c595a5..d3372936b1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -17,11 +17,12 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.CurrencyNotification import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications @Composable internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { @@ -29,16 +30,13 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { config = config, containerColor = TangemTheme.colors.background.tertiary, ) { content: ExchangeStatusBottomSheetConfig -> - ExchangeStatusBottomSheetContent(content = content) + ExchangeStatusBottomSheetContent(config = content.value) } } @Composable -private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) { - val config = content.value - Column( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - ) { +private fun ExchangeStatusBottomSheetContent(config: SwapTransactionsState) { + Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { SpacerH10() Text( text = stringResource(id = R.string.express_exchange_status_title), @@ -80,25 +78,39 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC showLink = config.showProviderLink, onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) }, ) - AnimatedContent( - targetState = config.notification, - label = "Exchange Status Notification Change", - ) { - it?.let { - val tint = when (config.activeStatus) { - ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention - ExchangeStatus.Failed -> TangemTheme.colors.icon.warning - else -> null - } - Notification( - config = it.config, - iconTint = tint, + if (config.notification != null) { + Notification(state = config.notification, activeStatus = config.activeStatus) + } + SpacerH24() + } +} + +@Composable +private fun Notification(state: ExchangeStatusNotifications, activeStatus: ExchangeStatus?) { + AnimatedContent( + targetState = state, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + label = "Exchange Status Notification Change", + ) { notification -> + when (notification) { + is ExchangeStatusNotifications.CommonNotification -> { + com.tangem.core.ui.components.notifications.Notification( + config = notification.config, + iconTint = when (activeStatus) { + ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention + ExchangeStatus.Failed -> TangemTheme.colors.icon.warning + else -> null + }, + containerColor = TangemTheme.colors.background.action, + ) + } + is ExchangeStatusNotifications.TokenRefunded -> { + CurrencyNotification( + config = notification.config, containerColor = TangemTheme.colors.background.action, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), ) } } - SpacerH24() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index fb15bd476d..fc2733a1ba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swaptx.ExchangeAnalyticsStatus import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent @@ -12,6 +13,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel @@ -38,6 +40,7 @@ internal class ExchangeStatusFactory( private val swapRepository: SwapRepository, private val quotesRepository: QuotesRepository, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val dispatchers: CoroutineDispatcherProvider, private val clickIntents: TokenDetailsClickIntents, @@ -81,12 +84,13 @@ internal class ExchangeStatusFactory( } } - suspend fun removeTransactionOnBottomSheetClosed(): TokenDetailsState { + suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false): TokenDetailsState { val state = currentStateProvider() val bottomSheetConfig = state.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return state val selectedTx = bottomSheetConfig.value - return if (selectedTx.activeStatus.isTerminal()) { + val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal + return if (shouldTerminate) { swapTransactionRepository.removeTransaction( userWalletId = userWalletId, fromCryptoCurrency = selectedTx.fromCryptoCurrency, @@ -105,12 +109,20 @@ internal class ExchangeStatusFactory( suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) { swapTxList.map { tx -> async { - if (tx.activeStatus.isTerminal()) { + val statusModel = getExchangeStatus(tx.txId) + val isRefundTerminalStatus = statusModel?.refundNetwork == null && + statusModel?.refundContractAddress == null && + tx.provider.type != ExchangeProviderType.DEX_BRIDGE + if (tx.activeStatus.isTerminal(isRefundTerminalStatus)) { tx } else { - val statusModel = getExchangeStatus(tx.txId) - swapTransactionsStateConverter - .updateTxStatus(tx, statusModel) + val addedRefundToken = addRefundCurrencyIfNeeded(statusModel, tx.provider.type) + swapTransactionsStateConverter.updateTxStatus( + tx = tx, + statusModel = statusModel, + refundToken = addedRefundToken, + isRefundTerminalStatus = isRefundTerminalStatus, + ) } } } @@ -143,6 +155,27 @@ internal class ExchangeStatusFactory( } } + /** + * For now do it only for dex-bridge provider + */ + private suspend fun addRefundCurrencyIfNeeded( + status: ExchangeStatusModel?, + type: ExchangeProviderType, + ): CryptoCurrency? { + status ?: return null + if (type != ExchangeProviderType.DEX_BRIDGE) return null + val refundNetwork = status.refundNetwork + val refundContractAddress = status.refundContractAddress + if (refundNetwork != null && refundContractAddress != null) { + return addCryptoCurrenciesUseCase( + userWalletId = userWalletId, + contractAddress = refundContractAddress, + networkId = refundNetwork, + ).getOrNull() + } + return null + } + private fun getExchangeStatusState( savedTransactions: List?, quotes: Set, @@ -157,23 +190,31 @@ internal class ExchangeStatusFactory( ) } - private fun ExchangeStatus?.isTerminal() = - this == ExchangeStatus.Refunded || this == ExchangeStatus.Finished || this == ExchangeStatus.Cancelled + private fun ExchangeStatus?.isTerminal(isRefundTerminal: Boolean): Boolean { + val needTerminalRefund = this == ExchangeStatus.Refunded && isRefundTerminal + return needTerminalRefund || + this == ExchangeStatus.Finished || + this == ExchangeStatus.Cancelled || + this == ExchangeStatus.TxFailed || + this == ExchangeStatus.Unknown + } private fun toAnalyticStatus(status: ExchangeStatus?): ExchangeAnalyticsStatus? { return when (status) { ExchangeStatus.New, ExchangeStatus.Waiting, - ExchangeStatus.WaitingTxHash, ExchangeStatus.Sending, ExchangeStatus.Confirming, ExchangeStatus.Exchanging, -> ExchangeAnalyticsStatus.InProgress + ExchangeStatus.WaitingTxHash -> ExchangeAnalyticsStatus.WaitingTxHash ExchangeStatus.Verifying -> ExchangeAnalyticsStatus.KYC ExchangeStatus.Failed -> ExchangeAnalyticsStatus.Fail + ExchangeStatus.TxFailed -> ExchangeAnalyticsStatus.FailTx ExchangeStatus.Finished -> ExchangeAnalyticsStatus.Done ExchangeStatus.Refunded -> ExchangeAnalyticsStatus.Refunded ExchangeStatus.Cancelled -> ExchangeAnalyticsStatus.Cancelled + ExchangeStatus.Unknown -> ExchangeAnalyticsStatus.Unknown else -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 9332738ab0..833a3a3f0b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -62,4 +62,8 @@ interface TokenDetailsClickIntents { fun onStakeBannerClick() fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) + + fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) + + fun onOpenUrlClick(url: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b885be8766..bffe54c894 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -102,6 +102,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, @@ -169,6 +170,7 @@ internal class TokenDetailsViewModel @Inject constructor( swapRepository = swapRepository, quotesRepository = quotesRepository, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase, swapTransactionStatusStore = swapTransactionStatusStore, dispatchers = dispatchers, clickIntents = this, @@ -728,7 +730,8 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onDismissBottomSheet() { - if (internalUiState.value.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) { + val bsContent = internalUiState.value.bottomSheetConfig?.content + if (bsContent is ExchangeStatusBottomSheetConfig) { viewModelScope.launch(dispatchers.main) { internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed() } @@ -750,6 +753,20 @@ internal class TokenDetailsViewModel @Inject constructor( router.openUrl(url) } + override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) { + if (internalUiState.value.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) { + viewModelScope.launch { + internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed(true) + } + } + internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + router.openTokenDetails(userWalletId, cryptoCurrency) + } + + override fun onOpenUrlClick(url: String) { + router.openUrl(url) + } + override fun onSwapPromoDismiss() { viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoTokenUseCase.neverToShow() diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 061ea006d3..934dbe9b97 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.13-714" +tangemBlockchainSdk = "release-app_5.13-718" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.13-376" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index f114465769..8bdae098a7 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,7 +1,8 @@ package com.tangem.lib.crypto +import com.tangem.blockchain.common.Amount import com.tangem.lib.crypto.models.* -import java.math.BigDecimal +import java.math.BigInteger interface TransactionManager { @@ -21,7 +22,7 @@ interface TransactionManager { @Throws(IllegalStateException::class) suspend fun getFee( networkId: String, - amountToSend: BigDecimal, + amountToSend: Amount, currencyToSend: Currency, destinationAddress: String, increaseBy: Int?, @@ -29,6 +30,9 @@ interface TransactionManager { derivationPath: String?, ): ProxyFees + @Throws(IllegalStateException::class) + suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees + @Throws(IllegalStateException::class) suspend fun updateWalletManager(networkId: String, derivationPath: String?) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt index 4b129ed3c4..620ff5a6c2 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt @@ -5,7 +5,7 @@ import java.math.BigDecimal import java.math.BigInteger internal fun BigInteger.toBigDecimal(decimals: Int): BigDecimal { - return BigDecimal(this).movePointLeft(decimals) + return this.toBigDecimal().movePointLeft(decimals) } internal fun BigInteger.toInstant(): Instant {