Updated on 2026-08-14
This commit is contained in:
commit
3a321cfbe9
45 changed files with 897 additions and 272 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit b5726a45bf51b7763c5967afae4d3d687676240b
|
||||
Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
private val _activeSessions: MutableSharedFlow<List<WalletConnectSession>> = MutableSharedFlow()
|
||||
override val activeSessions: Flow<List<WalletConnectSession>> = _activeSessions
|
||||
|
||||
private var currentSessions: List<WalletConnectSession> = emptyList()
|
||||
override var currentSessions: List<WalletConnectSession> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* @param projectId Project ID at https://cloud.walletconnect.com/
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ interface LegacyWalletConnectRepository {
|
|||
|
||||
val activeSessions: Flow<List<WalletConnectSession>>
|
||||
|
||||
val currentSessions: List<WalletConnectSession>
|
||||
|
||||
fun init(projectId: String)
|
||||
|
||||
fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>)
|
||||
|
|
|
|||
|
|
@ -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]+)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=3251-3466&t=eZb7ZPoNE8pQw45Y-4"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0"
|
||||
* >Figma component</a>
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<CryptoCurrency>> {
|
||||
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
|
||||
responseCurrenciesFactory.createCurrencies(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, CryptoCurrency> = either {
|
||||
val existingCurrencies =
|
||||
catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) {
|
||||
raise(it)
|
||||
}
|
||||
val foundToken = existingCurrencies
|
||||
.filterIsInstance<CryptoCurrency.Token>()
|
||||
.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<Throwable>.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<Throwable>.addCurrencies(userWalletId: UserWalletId, tokens: List<CryptoCurrency>) {
|
||||
catch(
|
||||
{ currenciesRepository.addCurrencies(userWalletId, tokens) },
|
||||
) {
|
||||
raise(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the [existingCurrencies] list contains a coin that corresponds
|
||||
* to the given [token].
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ interface TransactionRepository {
|
|||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
isSwap: Boolean,
|
||||
txExtras: TransactionExtras?,
|
||||
hash: String?,
|
||||
): TransactionData.Uncompiled?
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ internal class ExchangeStatusConverter : Converter<ExchangeStatusResponse, Excha
|
|||
txId = value.externalTxId,
|
||||
txExternalUrl = value.externalTxUrl,
|
||||
txExternalId = value.externalTxId,
|
||||
refundNetwork = value.refundNetwork,
|
||||
refundContractAddress = value.refundContractAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
|||
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class ExpressDataConverter : Converter<ExchangeDataResponseWithTxDetails, SwapDataModel> {
|
||||
|
||||
|
|
@ -24,19 +25,30 @@ internal class ExpressDataConverter : Converter<ExchangeDataResponseWithTxDetail
|
|||
dataResponse: ExchangeDataResponse,
|
||||
): ExpressTransactionModel {
|
||||
return if (transactionDto.txType == TxType.SWAP) {
|
||||
val otherNativeFeeWei = transactionDto.otherNativeFee?.let {
|
||||
if (it == "0") {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" }
|
||||
}
|
||||
}
|
||||
ExpressTransactionModel.DEX(
|
||||
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
|
||||
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
|
||||
txValue = transactionDto.txValue,
|
||||
txId = dataResponse.txId,
|
||||
txTo = transactionDto.txTo,
|
||||
txFrom = requireNotNull(transactionDto.txFrom),
|
||||
txData = requireNotNull(transactionDto.txData),
|
||||
txExtraId = transactionDto.txExtraId,
|
||||
otherNativeFeeWei = otherNativeFeeWei,
|
||||
gas = transactionDto.gas?.toBigIntegerOrNull() ?: error("gas is empty"),
|
||||
)
|
||||
} else {
|
||||
ExpressTransactionModel.CEX(
|
||||
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
|
||||
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
|
||||
txValue = transactionDto.txValue,
|
||||
txId = dataResponse.txId,
|
||||
txTo = transactionDto.txTo,
|
||||
externalTxId = requireNotNull(transactionDto.externalTxId),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ data class ExchangeStatusModel(
|
|||
val txId: String? = null,
|
||||
val txExternalUrl: String? = null,
|
||||
val txExternalId: String? = null,
|
||||
val refundNetwork: String? = null,
|
||||
val refundContractAddress: String? = null,
|
||||
)
|
||||
|
||||
enum class ExchangeStatus {
|
||||
|
|
@ -20,5 +22,6 @@ enum class ExchangeStatus {
|
|||
Finished,
|
||||
Refunded,
|
||||
Cancelled,
|
||||
TxFailed,
|
||||
Unknown,
|
||||
}
|
||||
|
|
@ -1,28 +1,38 @@
|
|||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
sealed class ExpressTransactionModel {
|
||||
|
||||
abstract val fromAmount: SwapAmount
|
||||
abstract val toAmount: SwapAmount
|
||||
abstract val txValue: String
|
||||
abstract val txId: String
|
||||
abstract val txTo: String
|
||||
abstract val txExtraId: String?
|
||||
|
||||
/**
|
||||
* @param txValue amount for tx, should use native coin decimals, this value will send as native amount in tx
|
||||
*/
|
||||
data class DEX(
|
||||
override val fromAmount: SwapAmount,
|
||||
override val toAmount: SwapAmount,
|
||||
override val txValue: String,
|
||||
override val txId: String,
|
||||
override val txTo: String,
|
||||
override val txExtraId: String?,
|
||||
val txFrom: String,
|
||||
val txData: String,
|
||||
val otherNativeFeeWei: BigDecimal?,
|
||||
val gas: BigInteger,
|
||||
) : ExpressTransactionModel()
|
||||
|
||||
data class CEX(
|
||||
override val fromAmount: SwapAmount,
|
||||
override val toAmount: SwapAmount,
|
||||
override val txValue: String,
|
||||
override val txId: String,
|
||||
override val txTo: String,
|
||||
override val txExtraId: String?,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import java.math.BigDecimal
|
|||
|
||||
sealed interface SwapState {
|
||||
|
||||
/**
|
||||
* @param txFee fee state uses for calculation and build transaction
|
||||
* @param txFeeIncludeOtherNativeFee fee state uses for display and included otherNativeFee (specific for bridge)
|
||||
*/
|
||||
data class QuotesLoadedState(
|
||||
val fromTokenInfo: TokenSwapInfo,
|
||||
val toTokenInfo: TokenSwapInfo,
|
||||
|
|
@ -23,6 +27,7 @@ sealed interface SwapState {
|
|||
val swapDataModel: SwapDataModel? = null,
|
||||
val txFee: TxFeeState,
|
||||
val warnings: List<Warning> = 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,
|
||||
|
|
|
|||
|
|
@ -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<SwapProvider, SwapState> {
|
||||
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<DataError, QuoteModel>,
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<SwapWarning>,
|
||||
) {
|
||||
// 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,
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SwapTransactionsState>) = 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<SavedSwapTransactionListModel>?,
|
||||
quotes: Set<Quote>,
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,4 +62,8 @@ interface TokenDetailsClickIntents {
|
|||
fun onStakeBannerClick()
|
||||
|
||||
fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig)
|
||||
|
||||
fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency)
|
||||
|
||||
fun onOpenUrlClick(url: String)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue