Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-19 22:29:41 +05:00
parent b2726d153c
commit f7f2ab7f88
35 changed files with 831 additions and 91 deletions

View file

@ -402,6 +402,28 @@
}
]
},
{
"id" : "vechain",
"symbol" : "VET",
"name" : "Vechain",
"networks" : [
{
"networkId" : "vechain/test"
}
]
},
{
"id": "vethor-token",
"symbol": "VTHO",
"name": "VeThor",
"networks": [
{
"networkId": "vechain/test",
"contractAddress": "0x0000000000000000000000000000456E65726779",
"decimalCount": 18
}
]
},
{
"id" : "tron",
"symbol" : "TRX",

View file

@ -215,6 +215,22 @@ internal object TokensDomainModule {
)
}
@Provides
@ViewModelScoped
fun provideGetFeePaidCryptoCurrencyStatusSyncUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetFeePaidCryptoCurrencyStatusSyncUseCase {
return GetFeePaidCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
dispatchers = dispatchers,
)
}
@Provides
@ViewModelScoped
fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {

View file

@ -3,10 +3,12 @@ package com.tangem.tap.features.send.redux
import com.tangem.Message
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
@ -28,10 +30,14 @@ object ReleaseSendState : Action
data class PrepareSendScreen(
val walletManager: WalletManager,
val feePaidCurrency: FeePaidCurrency,
val currency: CryptoCurrency,
val coinAmount: Amount?,
val coinRate: BigDecimal?,
val tokenAmount: Amount? = null,
val tokenRate: BigDecimal? = null,
val feeCurrencyRate: BigDecimal? = null,
val feeCurrencyDecimals: Int = 0,
) : SendScreenAction
// Address

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.Wallet
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.tap.common.extensions.scaleToFiat
@ -14,6 +15,7 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
class ReceiptReducer : SendInternalReducer {
private lateinit var sendState: SendState
@ -33,9 +35,10 @@ class ReceiptReducer : SendInternalReducer {
private fun handleRefresh(state: ReceiptState): SendState {
val wallet = sendState.walletManager?.wallet ?: return sendState
val feePaidCurrency = wallet.blockchain.feePaidCurrency()
val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount)
val symbols = determineSymbols(wallet, amountState.typeOfAmount)
val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount, feePaidCurrency)
val symbols = determineSymbols(wallet, amountState.typeOfAmount, feePaidCurrency)
val showBlank = !SendState.isReadyToSend()
val result = state.copy(
visibleTypeOfReceipt = layoutType,
@ -44,6 +47,10 @@ class ReceiptReducer : SendInternalReducer {
crypto = createCryptoType(symbols, showBlank),
tokenFiat = createTokenFiatType(symbols, showBlank),
tokenCrypto = createTokenCryptoType(symbols, showBlank),
customTokenFiat = createCustomTokenFiat(symbols, showBlank),
customTokenCrypto = createCustomTokenCrypto(symbols, showBlank),
sameCurrencyFiat = createSameCurrencyFiat(symbols, showBlank),
sameCurrencyCrypto = createSameCurrencyCrypto(symbols, showBlank),
)
return updateLastState(sendState.copy(receiptState = result), result)
}
@ -170,7 +177,143 @@ class ReceiptReducer : SendInternalReducer {
}
}
private fun determineSymbols(wallet: Wallet, amountType: AmountType): ReceiptSymbols {
private fun createCustomTokenCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenCrypto {
val feeValue = feeState.getCurrentFeeValue()
if (showBlank) {
return ReceiptTokenCrypto("0", feeValue.stripZeroPlainString(), "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
return if (sendState.currencyIsConvertible() && sendState.feeIsConvertible()) {
val currencyConverter = when (amountState.typeOfAmount) {
is AmountType.Token -> sendState.tokenConverter!!
else -> sendState.coinConverter!!
}
val currencyFiat = currencyConverter.toFiatUnscaled(tokensToSend)
val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeValue)
val totalFiat = currencyFiat.plus(feeFiat)
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols,
)
} else {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(),
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
symbols = symbols,
)
}
}
private fun createCustomTokenFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenFiat {
val feeCoin = feeState.getCurrentFeeValue()
if (showBlank) {
val feeFiat = convertToFiatPrecision(feeCoin)
return ReceiptTokenFiat("0", feeFiat, "0", "0", "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
return if (sendState.currencyIsConvertible() && sendState.feeIsConvertible()) {
val currencyConverter = when (amountState.typeOfAmount) {
is AmountType.Token -> sendState.tokenConverter!!
else -> sendState.coinConverter!!
}
val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeCoin)
val amountFiat = currencyConverter.toFiatUnscaled(tokensToSend)
val totalFiat = amountFiat.plus(feeFiat)
ReceiptTokenFiat(
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
willSentToken = tokensToSend.scaleToFiat(true).stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols,
)
} else {
ReceiptTokenFiat(
amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols,
)
}
}
private fun createSameCurrencyCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptCrypto {
val feeCrypto = feeState.getCurrentFeeValue()
val feeFiat = convertToFiatPrecision(feeCrypto)
if (showBlank) {
return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols)
}
val isToken = amountState.typeOfAmount is AmountType.Token
if (feeState.feeIsIncluded) {
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(value = amountState.amountToSendCrypto, isToken = isToken),
symbols = symbols,
)
} else {
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(value = totalCrypto, isToken = isToken),
symbols = symbols,
)
}
}
private fun createSameCurrencyFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptFiat {
val feeCrypto = feeState.getCurrentFeeValue()
val isToken = amountState.typeOfAmount is AmountType.Token
val feeFiat = convertToFiatPrecision(feeCrypto, isToken = isToken)
if (showBlank) {
return ReceiptFiat("0", feeFiat, "0", "0", symbols)
}
return if (feeState.feeIsIncluded) {
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto), isToken = isToken)
val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = amountState.amountToSendCrypto.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols,
)
} else {
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken)
val totalFiat = convertToFiatPrecision(totalAmountCrypto, isToken = isToken)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = totalAmountCrypto.scaleToFiat().stripZeroPlainString(),
symbols = symbols,
)
}
}
private fun determineSymbols(
wallet: Wallet,
amountType: AmountType,
feePaidCurrency: FeePaidCurrency,
): ReceiptSymbols {
return ReceiptSymbols(
fiat = store.state.globalState.appCurrency.code,
crypto = wallet.blockchain.currency,
@ -178,17 +321,52 @@ class ReceiptReducer : SendInternalReducer {
is AmountType.Token -> amountType.token.symbol
else -> null
},
fee = when (feePaidCurrency) {
FeePaidCurrency.Coin -> wallet.blockchain.currency
FeePaidCurrency.SameCurrency -> store.state.sendState.currency?.symbol
is FeePaidCurrency.Token -> feePaidCurrency.token.symbol
},
)
}
private fun determineLayoutType(mainCurrencyType: MainCurrencyType, amountType: AmountType): ReceiptLayoutType {
private fun determineLayoutType(
mainCurrencyType: MainCurrencyType,
amountType: AmountType,
feePaidCurrency: FeePaidCurrency,
): ReceiptLayoutType {
return when (mainCurrencyType) {
MainCurrencyType.FIAT -> when (amountType) {
MainCurrencyType.FIAT -> determineFiatLayoutType(feePaidCurrency, amountType)
MainCurrencyType.CRYPTO -> determineCryptoLayoutType(feePaidCurrency, amountType)
}
}
private fun determineFiatLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType {
return when (feePaidCurrency) {
is FeePaidCurrency.Token -> {
val amountToken = (amountType as? AmountType.Token)?.token
val sameToken =
feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true)
if (sameToken) ReceiptLayoutType.SAME_CURRENCY_FIAT else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT
}
FeePaidCurrency.Coin -> when (amountType) {
AmountType.Coin -> ReceiptLayoutType.FIAT
is AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT
AmountType.Reserve -> ReceiptLayoutType.UNKNOWN
}
MainCurrencyType.CRYPTO -> when (amountType) {
FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY_FIAT
}
}
private fun determineCryptoLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType {
return when (feePaidCurrency) {
is FeePaidCurrency.Token -> {
val amountToken = (amountType as? AmountType.Token)?.token
val sameToken =
feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true)
if (sameToken) ReceiptLayoutType.SAME_CURRENCY else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN
}
FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY
FeePaidCurrency.Coin -> when (amountType) {
AmountType.Coin -> ReceiptLayoutType.CRYPTO
is AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO
AmountType.Reserve -> ReceiptLayoutType.UNKNOWN

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
@ -91,29 +90,40 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer {
val walletManager = action.walletManager
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = amountToExtract.decimals
val feePaidInNetworkCurrency = isFeePaidInNetworkCurrency(walletManager.wallet.blockchain)
val canIncludeFee = canIncludeFee(
typeOfAmount = amountToExtract.type,
feePaidCurrency = action.feePaidCurrency,
)
return sendState.copy(
walletManager = walletManager,
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) },
customFeeConverter = action.feeCurrencyRate?.let { CurrencyConverter(it, action.feeCurrencyDecimals) },
amountState = sendState.amountState.copy(
feePaidInCurrencyNetworkCurrency = feePaidInNetworkCurrency,
amountToExtract = amountToExtract,
typeOfAmount = amountToExtract.type,
balanceCrypto = amountToExtract.value ?: BigDecimal.ZERO,
),
feeState = sendState.feeState.copy(
includeFeeSwitcherIsEnabled = feePaidInNetworkCurrency || isCoinAmount(amountToExtract.type),
),
feeState = sendState.feeState.copy(includeFeeSwitcherIsEnabled = canIncludeFee),
canIncludeFee = canIncludeFee,
currency = action.currency,
)
}
private fun isFeePaidInNetworkCurrency(blockchain: Blockchain): Boolean =
// blockchain.tokenTransactionFeePaidInNetworkCurrency()
blockchain.feePaidCurrency() == FeePaidCurrency.SameCurrency // TODO [REDACTED_TASK_KEY]
private fun canIncludeFee(typeOfAmount: AmountType, feePaidCurrency: FeePaidCurrency): Boolean {
return when (feePaidCurrency) {
FeePaidCurrency.Coin -> typeOfAmount == AmountType.Coin
FeePaidCurrency.SameCurrency -> true
is FeePaidCurrency.Token -> {
val sendToken = (typeOfAmount as? AmountType.Token)?.token ?: return false
private fun isCoinAmount(typeOfAmount: AmountType): Boolean = typeOfAmount == AmountType.Coin
sendToken.contractAddress.equals(feePaidCurrency.token.contractAddress, ignoreCase = true) &&
sendToken.name.equals(feePaidCurrency.token.name, ignoreCase = true) &&
sendToken.symbol.equals(feePaidCurrency.token.symbol, ignoreCase = true)
}
}
}
}
internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState {

View file

@ -2,7 +2,15 @@ package com.tangem.tap.features.send.redux.states
// Shows only one type of the layout
enum class ReceiptLayoutType {
UNKNOWN, FIAT, CRYPTO, TOKEN_FIAT, TOKEN_CRYPTO
UNKNOWN,
FIAT,
CRYPTO,
TOKEN_FIAT,
TOKEN_CRYPTO,
FEE_IN_CUSTOM_TOKEN,
FEE_IN_CUSTOM_TOKEN_FIAT,
SAME_CURRENCY,
SAME_CURRENCY_FIAT,
}
data class ReceiptState(
@ -11,6 +19,10 @@ data class ReceiptState(
val crypto: ReceiptCrypto? = null,
val tokenFiat: ReceiptTokenFiat? = null,
val tokenCrypto: ReceiptTokenCrypto? = null,
val customTokenFiat: ReceiptTokenFiat? = null,
val customTokenCrypto: ReceiptTokenCrypto? = null,
val sameCurrencyFiat: ReceiptFiat? = null,
val sameCurrencyCrypto: ReceiptCrypto? = null,
val mainCurrency: MainCurrency? = null,
) : SendScreenState {
override val stateId: StateId = StateId.RECEIPT
@ -20,6 +32,7 @@ data class ReceiptSymbols(
val fiat: String,
val crypto: String,
val token: String? = null,
val fee: String? = null,
)
data class ReceiptFiat(

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.text.DecimalDigitsInputFilter
@ -31,8 +32,10 @@ interface SendScreenState : StateType, IdStateHolder
data class SendState(
val walletManager: WalletManager? = null,
val currency: CryptoCurrency? = null,
val coinConverter: CurrencyConverter? = null,
val tokenConverter: CurrencyConverter? = null,
val customFeeConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressState: AddressState = AddressState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
@ -44,6 +47,7 @@ data class SendState(
val dialog: StateDialog? = null,
val externalTransactionData: ExternalTransactionData? = null,
val isSuccessSend: Boolean = false,
val canIncludeFee: Boolean = false,
) : SendScreenState {
override val stateId: StateId = StateId.SEND_SCREEN
@ -99,6 +103,9 @@ data class SendState(
fun coinIsConvertible(): Boolean = coinConverter != null
fun tokenIsConvertible(): Boolean = tokenConverter != null
fun currencyIsConvertible(): Boolean = coinConverter != null || tokenConverter != null
fun feeIsConvertible(): Boolean = customFeeConverter != null
fun mainCurrencyCanBeSwitched(): Boolean {
return when (amountState.typeOfAmount) {
AmountType.Coin -> coinIsConvertible()
@ -137,16 +144,13 @@ data class AmountState(
val decimalSeparator: String = ".",
val error: TapError? = null,
val inputIsEnabled: Boolean = true,
private val feePaidInCurrencyNetworkCurrency: Boolean = false,
) : SendScreenState {
override val stateId: StateId = StateId.AMOUNT
fun isReady(): Boolean = error == null && !amountToSendCrypto.isZero()
private fun isCoinAmount(): Boolean = typeOfAmount == AmountType.Coin
fun canIncludeFee(): Boolean = isCoinAmount() || feePaidInCurrencyNetworkCurrency
fun canIncludeFee(): Boolean = store.state.sendState.canIncludeFee
fun createMainCurrency(type: MainCurrencyType, canSwitched: Boolean): MainCurrency {
return if (!canSwitched) {

View file

@ -306,7 +306,7 @@ internal class SendStateSubscriber(
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleReceiptState(fg: SendFragment, state: ReceiptState, feeProgressState: ProgressState) =
private fun handleReceiptState(fg: SendFragment, state: ReceiptState, feeProgressState: ProgressState) {
with(fg.binding.clReceiptContainer) {
val mainLayout = clReceiptContainer as ViewGroup
val totalLayout = llTotalContainer.llTotal as ViewGroup
@ -410,9 +410,96 @@ internal class SendStateSubscriber(
}
llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString())
}
else -> {}
ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN -> {
val receipt = state.customTokenCrypto ?: return
totalLayout.show(false)
totalTokenLayout.show(true)
tvReceiptAmountValue.update(
"${receipt.amountToken} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.fee}")
val willSent = SpannableStringBuilder()
.bold {
append(roughOrEmpty(receipt.totalFiat)).append(" ")
append(receipt.symbols.fiat)
}
llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString())
}
ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT -> {
val receipt = state.customTokenFiat ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
)
}
val willSent = getString(
R.string.send_total_subtitle_asset_format,
"${receipt.symbols.token ?: receipt.symbols.crypto} ${receipt.willSentToken}",
"${receipt.symbols.fee} ${receipt.willSentFeeCoin}",
)
llTotalContainer.tvWillBeSentValue.update(willSent)
}
ReceiptLayoutType.SAME_CURRENCY -> {
val receipt = state.sameCurrencyCrypto ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update(
"${receipt.amountCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${receipt.totalCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
}
if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) {
llTotalContainer.tvWillBeSentValue.hide()
} else {
llTotalContainer.tvWillBeSentValue.show()
llTotalContainer.tvWillBeSentValue.update(
getString(
R.string.send_total_subtitle_fiat_format,
"${receipt.willSentFiat} ${receipt.symbols.fiat}",
"${receipt.feeFiat} ${receipt.symbols.fiat}",
),
)
}
}
ReceiptLayoutType.SAME_CURRENCY_FIAT -> {
val receipt = state.sameCurrencyFiat ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
)
}
val willSent = getString(
R.string.send_total_subtitle_format,
"${receipt.willSentCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
llTotalContainer.tvWillBeSentValue.update(willSent)
}
ReceiptLayoutType.UNKNOWN, null -> {}
}
}
}
private companion object {
const val ROUGH_SIGN = ""

View file

@ -210,10 +210,14 @@ object TradeCryptoMiddleware {
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
currency = currency,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = action.coinFiatRate,
tokenAmount = sendableAmount,
tokenRate = action.tokenFiatRate,
feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate,
feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0,
),
)
@ -262,8 +266,12 @@ object TradeCryptoMiddleware {
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
currency = currency,
coinAmount = amountToSend,
coinRate = cryptoStatus.value.fiatRate,
feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate,
feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0,
),
)
}

View file

@ -715,7 +715,7 @@
<string name="warning_rate_app_message">Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше</string>
<string name="warning_rate_app_title">Нравится Tangem?</string>
<string name="warning_rent_fee_title">Необходима плата за аренду сети</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s - это токен в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести депозит в размере %4$s (%5$s), чтобы покрыть комиссию сети.</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.</string>
<string name="warning_send_blocked_funds_for_fee_title">Недостаточно %1$s для оплаты комиссии сети</string>
<string name="warning_send_blocked_pending_transactions_message">Отправка средств станет доступной после завершения транзакции %s</string>
<string name="warning_send_blocked_pending_transactions_title">Транзакция в обработке</string>

View file

@ -728,7 +728,7 @@
<string name="warning_rate_app_message">Your review keeps us motivated to make Tangem Wallet even better</string>
<string name="warning_rate_app_title">Enjoying Tangem?</string>
<string name="warning_rent_fee_title">Network rent fee required</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s is a token in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee.</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee.</string>
<string name="warning_send_blocked_funds_for_fee_title">Insufficient %1$s to cover network fee</string>
<string name="warning_send_blocked_pending_transactions_message">Sending funds will be available once the %s transaction is complete</string>
<string name="warning_send_blocked_pending_transactions_title">Transaction pending</string>

View file

@ -47,6 +47,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"NEAR", "NEAR/test" -> R.drawable.img_near_22
"decimal", "decimal/test" -> R.drawable.img_decimal_22
"xdc", "xdc/test" -> R.drawable.img_xdc_22
"vechain", "vechain/test" -> R.drawable.img_vechain_22
else -> R.drawable.ic_alert_24
}
}
@ -95,6 +96,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"near-protocol", "near-protocol/test" -> R.drawable.img_near_22
"decimal", "decimal/test" -> R.drawable.img_decimal_22
"xdc-network", "xdc-network/test" -> R.drawable.img_xdc_22
"vechain", "vechain/test" -> R.drawable.img_vechain_22
else -> R.drawable.ic_alert_24
}
}
@ -140,6 +142,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"near" -> R.drawable.img_near_22
"decimal" -> R.drawable.img_decimal_22
"xdce-crowd-sale" -> R.drawable.img_xdc_22
"vechain" -> R.drawable.img_vechain_22
else -> R.drawable.ic_alert_24
}
}
@ -188,6 +191,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"NEAR", "NEAR/test" -> R.drawable.ic_near_22
"decimal", "decimal/test" -> R.drawable.ic_decimal_22
"xdc", "xdc/test" -> R.drawable.ic_xdc_22
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
else -> R.drawable.ic_alert_24
}
}
@ -236,6 +240,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"near-protocol", "near-protocol/test" -> R.drawable.ic_near_22
"decimal", "decimal/test" -> R.drawable.ic_decimal_22
"xdc-network", "xdc-network/test" -> R.drawable.ic_xdc_22
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:pathData="M17.25,5H16.135C15.863,5 15.613,5.166 15.496,5.423L12.561,11.859L12.558,11.851L11.777,13.565L11.78,13.573L10.999,15.288L7.096,6.717H8.209C8.481,6.717 8.73,6.883 8.848,7.141L11.396,12.7L12.177,10.986L10.12,6.499C9.704,5.584 8.825,5 7.862,5H4.75L5.529,6.717H5.531L10.215,17H11.777L17.25,5Z" />
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#82BE00"/>
<path
android:pathData="M17.25,5H16.135C15.863,5 15.613,5.166 15.496,5.423L12.561,11.859L12.558,11.851L11.777,13.565L11.78,13.573L10.999,15.288L7.096,6.717H8.209C8.481,6.717 8.73,6.883 8.848,7.141L11.396,12.7L12.177,10.986L10.12,6.499C9.704,5.584 8.825,5 7.862,5H4.75L5.529,6.717H5.531L10.215,17H11.777L17.25,5Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -30,6 +30,7 @@ internal object TokensDataModule {
tangemExpressApi: TangemExpressApi,
userTokensStore: UserTokensStore,
userWalletsStore: UserWalletsStore,
walletManagersFacade: WalletManagersFacade,
assetsStore: AssetsStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
@ -38,6 +39,7 @@ internal object TokensDataModule {
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
userTokensStore = userTokensStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
assetsStore = assetsStore,
cacheRegistry = cacheRegistry,

View file

@ -23,8 +23,10 @@ import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@Suppress("LargeClass", "LongParameterList")
internal class DefaultCurrenciesRepository(
@ -39,6 +42,7 @@ internal class DefaultCurrenciesRepository(
private val tangemExpressApi: TangemExpressApi,
private val userTokensStore: UserTokensStore,
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val assetsStore: AssetsStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
@ -323,6 +327,32 @@ internal class DefaultCurrenciesRepository(
}
}
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
val blockchain = Blockchain.fromId(currency.network.id.value)
return when (val feePaidCurrency = blockchain.feePaidCurrency()) {
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
is FeePaidSdkCurrency.Token -> {
val balance = walletManagersFacade.tokenBalance(
userWalletId = userWalletId,
network = currency.network,
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,
decimals = feePaidCurrency.token.decimals,
id = feePaidCurrency.token.id,
)
FeePaidCurrency.Token(
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,
balance = balance,
)
}
}
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createCurrencies(

View file

@ -76,8 +76,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"decimal/test" -> Blockchain.DecimalTestnet
"xdc-network" -> Blockchain.XDC
"xdc-network/test" -> Blockchain.XDCTestnet
"TODO" -> Blockchain.Vechain // TODO [REDACTED_TASK_KEY]
"TODO/test" -> Blockchain.VechainTestnet // TODO [REDACTED_TASK_KEY]
"vechain" -> Blockchain.Vechain
"vechain/test" -> Blockchain.VechainTestnet
"aptos" -> Blockchain.Aptos
"aptos/test" -> Blockchain.AptosTestnet
else -> null
@ -157,8 +157,8 @@ fun Blockchain.toNetworkId(): String {
Blockchain.DecimalTestnet -> "decimal/test"
Blockchain.XDC -> "xdc-network"
Blockchain.XDCTestnet -> "xdc-network/test"
Blockchain.Vechain -> "TODO" // TODO [REDACTED_TASK_KEY]
Blockchain.VechainTestnet -> "TODO/test" // TODO [REDACTED_TASK_KEY]
Blockchain.Vechain -> "vechain"
Blockchain.VechainTestnet -> "vechain/test"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
}
@ -209,8 +209,7 @@ fun Blockchain.toCoinId(): String {
Blockchain.NearTestnet -> "near/test"
Blockchain.Decimal, Blockchain.DecimalTestnet -> "decimal"
Blockchain.XDC, Blockchain.XDCTestnet -> "xdce-crowd-sale"
Blockchain.Vechain -> "TODO" // TODO [REDACTED_TASK_KEY]
Blockchain.VechainTestnet -> "TODO/test" // TODO [REDACTED_TASK_KEY]
Blockchain.Vechain, Blockchain.VechainTestnet -> "vechain"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
Blockchain.Unknown -> "unknown"
@ -240,6 +239,4 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Ducatus,
Blockchain.Vechain, // TODO [REDACTED_TASK_KEY]
Blockchain.VechainTestnet, // TODO [REDACTED_TASK_KEY]
)

View file

@ -565,6 +565,35 @@ class DefaultWalletManagersFacade(
return walletManager.wallet.recentTransactions.mapNotNull(transactionDataConverter::convert)
}
override suspend fun tokenBalance(
userWalletId: UserWalletId,
network: Network,
name: String,
symbol: String,
contractAddress: String,
decimals: Int,
id: String?,
): BigDecimal {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
requireNotNull(walletManager) { "Unable to get a wallet manager for blockchain: $blockchain" }
return walletManager.wallet.fundsAvailable(
AmountType.Token(
token = Token(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimals = decimals,
id = id,
),
),
)
}
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
if (tokens.isEmpty()) return

View file

@ -270,4 +270,15 @@ interface WalletManagersFacade {
blockchain: Blockchain,
derivationPath: String?,
): List<TxHistoryItem>
@Suppress("LongParameterList")
suspend fun tokenBalance(
userWalletId: UserWalletId,
network: Network,
name: String,
symbol: String,
contractAddress: String,
decimals: Int,
id: String? = null,
): BigDecimal
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.tokens.model
import java.math.BigDecimal
sealed class FeePaidCurrency {
object Coin : FeePaidCurrency()
object SameCurrency : FeePaidCurrency()
data class Token(
val tokenId: CryptoCurrency.ID,
val name: String,
val symbol: String,
val contractAddress: String,
val balance: BigDecimal,
) : FeePaidCurrency()
}

View file

@ -15,6 +15,14 @@ sealed class CryptoCurrencyWarning {
val coinCurrency: CryptoCurrency,
) : CryptoCurrencyWarning()
data class CustomTokenNotEnoughForFee(
val currency: CryptoCurrency,
val feeCurrency: CryptoCurrency?,
val networkName: String,
val feeCurrencyName: String,
val feeCurrencySymbol: String,
) : CryptoCurrencyWarning()
object SomeNetworksUnreachable : CryptoCurrencyWarning()
data class SomeNetworksNoAccount(

View file

@ -4,6 +4,7 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -11,10 +12,12 @@ import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
/**
* Use case to determine which TokenActions are available for a [CryptoCurrency]
@ -104,7 +107,13 @@ class GetCryptoCurrencyActionsUseCase(
activeList.add(TokenActionsState.ActionState.Receive(true))
// send
if (isSendDisabled(cryptoCurrencyStatus = cryptoCurrencyStatus, coinStatus = coinStatus)) {
if (
isSendDisabled(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
) {
disabledList.add(TokenActionsState.ActionState.Send(false))
} else {
activeList.add(TokenActionsState.ActionState.Send(true))
@ -157,13 +166,41 @@ class GetCryptoCurrencyActionsUseCase(
return activeList + disabledList
}
private fun isSendDisabled(
private suspend fun isSendDisabled(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean = cryptoCurrencyStatus.value.amount.isNullOrZero() ||
coinStatus?.value?.amount.isNullOrZero() ||
currenciesRepository.hasPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
): Boolean {
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, cryptoCurrencyStatus.currency)
val notEnoughBalanceForFee = isNotEnoughBalanceForFee(
feePaidCurrency = feePaidCurrency,
tokenStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
return cryptoCurrencyStatus.value.amount.isNullOrZero() ||
notEnoughBalanceForFee ||
currenciesRepository.hasPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
}
private fun isNotEnoughBalanceForFee(
feePaidCurrency: FeePaidCurrency,
tokenStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {
return when (feePaidCurrency) {
FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero()
FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero()
is FeePaidCurrency.Token -> {
val feePaidTokenBalance = feePaidCurrency.balance
!tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero()
}
}
}
private fun BigDecimal?.isZero(): Boolean {
return this?.signum() == 0
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.tokens
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
@ -47,6 +48,7 @@ class GetCurrencyWarningsUseCase(
)
return combine(
getCoinRelatedWarnings(
userWalletId = userWalletId,
operations = operations,
networkId = currency.network.id,
currencyId = currency.id,
@ -136,6 +138,7 @@ class GetCurrencyWarningsUseCase(
@Suppress("LongParameterList")
private suspend fun getCoinRelatedWarnings(
userWalletId: UserWalletId,
operations: CurrenciesStatusesOperations,
networkId: Network.ID,
currencyId: CryptoCurrency.ID,
@ -162,14 +165,11 @@ class GetCurrencyWarningsUseCase(
if (currenciesRepository.hasPendingTransactions(tokenStatus, coinStatus)) {
add(CryptoCurrencyWarning.HasPendingTransactions(coinStatus.currency.symbol))
}
if (!tokenStatus.value.amount.isZero() && coinStatus.value.amount.isZero()) {
add(
CryptoCurrencyWarning.BalanceNotEnoughForFee(
tokenCurrency = tokenStatus.currency,
coinCurrency = coinStatus.currency,
),
)
}
getFeeWarning(
userWalletId = userWalletId,
coinStatus = coinStatus,
tokenStatus = tokenStatus,
)?.let(::add)
}
}
else -> listOf(CryptoCurrencyWarning.SomeNetworksUnreachable)
@ -177,6 +177,72 @@ class GetCurrencyWarningsUseCase(
}
}
private suspend fun getFeeWarning(
userWalletId: UserWalletId,
coinStatus: CryptoCurrencyStatus,
tokenStatus: CryptoCurrencyStatus,
): CryptoCurrencyWarning? {
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency)
return when {
feePaidCurrency is FeePaidCurrency.Coin &&
!tokenStatus.value.amount.isZero() &&
coinStatus.value.amount.isZero() -> {
CryptoCurrencyWarning.BalanceNotEnoughForFee(
tokenCurrency = tokenStatus.currency,
coinCurrency = coinStatus.currency,
)
}
feePaidCurrency is FeePaidCurrency.SameCurrency && !tokenStatus.value.amount.isZero() -> {
CryptoCurrencyWarning.BalanceNotEnoughForFee(
tokenCurrency = tokenStatus.currency,
coinCurrency = coinStatus.currency,
)
}
feePaidCurrency is FeePaidCurrency.Token -> {
val feePaidTokenBalance = feePaidCurrency.balance
if (!tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero()) {
constructTokenBalanceNotEnoughWarning(
userWalletId = userWalletId,
tokenStatus = tokenStatus,
feePaidToken = feePaidCurrency,
)
} else {
null
}
}
else -> null
}
}
private suspend fun constructTokenBalanceNotEnoughWarning(
userWalletId: UserWalletId,
tokenStatus: CryptoCurrencyStatus,
feePaidToken: FeePaidCurrency.Token,
): CryptoCurrencyWarning {
val token = currenciesRepository
.getMultiCurrencyWalletCurrenciesSync(userWalletId)
.find {
it is CryptoCurrency.Token && it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true)
}
return if (token != null) {
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
currency = tokenStatus.currency,
feeCurrency = token,
networkName = token.network.name,
feeCurrencyName = feePaidToken.name,
feeCurrencySymbol = feePaidToken.symbol,
)
} else {
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
currency = tokenStatus.currency,
feeCurrency = null,
networkName = tokenStatus.currency.network.name,
feeCurrencyName = feePaidToken.name,
feeCurrencySymbol = feePaidToken.symbol,
)
}
}
private fun getNetworkUnavailableWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? {
return (currencyStatus.value as? CryptoCurrencyStatus.Unreachable)?.let {
CryptoCurrencyWarning.SomeNetworksUnreachable

View file

@ -0,0 +1,51 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
class GetFeePaidCryptoCurrencyStatusSyncUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<TokenListError, CryptoCurrencyStatus?> {
val cryptoCurrency = cryptoCurrencyStatus.currency
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, cryptoCurrency)
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
)
return either {
when (feePaidCurrency) {
FeePaidCurrency.Coin -> getCryptoCurrency(operations, cryptoCurrency.id)
FeePaidCurrency.SameCurrency -> cryptoCurrencyStatus
is FeePaidCurrency.Token -> getCryptoCurrency(operations, feePaidCurrency.tokenId)
}
}
}
private suspend fun getCryptoCurrency(
operations: CurrenciesStatusesOperations,
cryptoCurrencyId: CryptoCurrency.ID,
): CryptoCurrencyStatus? {
return operations.getCurrencyStatusSync(cryptoCurrencyId).getOrNull()
}
}

View file

@ -36,9 +36,14 @@ sealed class TradeCryptoAction : Action {
val tokenCurrency: CryptoCurrency.Token,
val tokenFiatRate: BigDecimal?,
val coinFiatRate: BigDecimal?,
val feeCurrencyStatus: CryptoCurrencyStatus?,
) : New()
data class SendCoin(val userWallet: UserWallet, val coinStatus: CryptoCurrencyStatus) : New()
data class SendCoin(
val userWallet: UserWallet,
val coinStatus: CryptoCurrencyStatus,
val feeCurrencyStatus: CryptoCurrencyStatus?,
) : New()
data class Swap(val cryptoCurrency: CryptoCurrency) : New()
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens.repository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -175,4 +176,9 @@ interface CurrenciesRepository {
* @param coinStatus main currency status in [cryptoCurrencyStatus] network
*/
fun hasPendingTransactions(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean
/**
* Retrieves fee paid currency for specific [currency].
*/
suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency
}

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -115,4 +116,8 @@ internal class MockCurrenciesRepository(
): Boolean {
return false
}
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
return FeePaidCurrency.Coin
}
}

View file

@ -647,7 +647,10 @@ internal class SwapInteractorImpl @Inject constructor(
gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(),
)
}
Blockchain.Vechain -> TODO("[REDACTED_TASK_KEY]")
Blockchain.Vechain -> Fee.Vechain(
amount = feeAmount,
gasPriceCoef = fee.gasLimit,
)
Blockchain.Aptos -> {
Fee.Aptos(
amount = feeAmount,
@ -1342,7 +1345,7 @@ internal class SwapInteractorImpl @Inject constructor(
return when (this) {
is Fee.Common -> 0
is Fee.Ethereum -> gasLimit.toInt()
is Fee.Vechain -> TODO("[REDACTED_TASK_KEY]")
is Fee.Vechain -> gasPriceCoef
is Fee.Aptos -> amount.longValue?.div(gasUnitPrice)?.toInt() ?: 0
}
}

View file

@ -30,7 +30,9 @@ internal class TokenDetailsNotificationsAnalyticsSender(
private fun getEvent(notification: TokenDetailsNotification): AnalyticsEvent? {
return when (notification) {
is TokenDetailsNotification.NetworkFee -> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
is TokenDetailsNotification.NetworkFee,
is TokenDetailsNotification.NetworkFeeWithBuyButton,
-> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
currency = cryptoCurrency,
)
is TokenDetailsNotification.NetworksUnreachable,

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.features.tokendetails.impl.R
@ -62,28 +63,55 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
)
data class NetworkFee(
private val feeInfo: CryptoCurrencyWarning.BalanceNotEnoughForFee,
private val onBuyClick: () -> Unit,
private val currency: CryptoCurrency,
private val networkName: String,
private val feeCurrencyName: String,
private val feeCurrencySymbol: String,
) : Warning(
title = TextReference.Res(
id = R.string.warning_send_blocked_funds_for_fee_title,
formatArgs = wrappedList(feeInfo.coinCurrency.name),
formatArgs = wrappedList(feeCurrencyName),
),
subtitle = TextReference.Res(
id = R.string.warning_send_blocked_funds_for_fee_message,
formatArgs = wrappedList(
feeInfo.tokenCurrency.name,
feeInfo.coinCurrency.name,
feeInfo.tokenCurrency.name,
feeInfo.coinCurrency.name,
feeInfo.coinCurrency.symbol,
currency.name,
networkName,
currency.name,
feeCurrencyName,
feeCurrencySymbol,
),
),
iconResId = feeInfo.tokenCurrency.networkIconResId,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
iconResId = currency.networkIconResId,
)
data class NetworkFeeWithBuyButton(
private val currency: CryptoCurrency,
private val networkName: String,
private val feeCurrencyName: String,
private val feeCurrencySymbol: String,
private val onBuyClick: () -> Unit,
) : Warning(
title = TextReference.Res(
id = R.string.warning_send_blocked_funds_for_fee_title,
formatArgs = wrappedList(feeCurrencyName),
),
subtitle = TextReference.Res(
id = R.string.warning_send_blocked_funds_for_fee_message,
formatArgs = wrappedList(
currency.name,
networkName,
currency.name,
feeCurrencyName,
feeCurrencySymbol,
),
),
iconResId = currency.networkIconResId,
buttonsState =
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(
id = R.string.common_buy_currency,
formatArgs = wrappedList(feeInfo.coinCurrency.symbol),
formatArgs = wrappedList(feeCurrencySymbol),
),
onClick = onBuyClick,
),

View file

@ -25,10 +25,32 @@ internal class TokenDetailsNotificationConverter(
private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification {
return when (warning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.NetworkFee(
feeInfo = warning,
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.NetworkFeeWithBuyButton(
currency = warning.tokenCurrency,
networkName = warning.coinCurrency.name,
feeCurrencyName = warning.coinCurrency.name,
feeCurrencySymbol = warning.coinCurrency.symbol,
onBuyClick = { clickIntents.onBuyCoinClick(warning.coinCurrency) },
)
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val feeCurrency = warning.feeCurrency
if (feeCurrency != null) {
TokenDetailsNotification.NetworkFeeWithBuyButton(
currency = warning.currency,
networkName = feeCurrency.network.name,
feeCurrencyName = warning.feeCurrencyName,
feeCurrencySymbol = warning.feeCurrencySymbol,
onBuyClick = { clickIntents.onBuyCoinClick(feeCurrency) },
)
} else {
TokenDetailsNotification.NetworkFee(
currency = warning.currency,
networkName = warning.networkName,
feeCurrencyName = warning.feeCurrencyName,
feeCurrencySymbol = warning.feeCurrencySymbol,
)
}
}
is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.ExistentialDeposit(
existentialInfo = warning,
)

View file

@ -74,6 +74,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -356,23 +357,35 @@ internal class TokenDetailsViewModel @Inject constructor(
val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return
viewModelScope.launch(dispatchers.main) {
val maybeFeeCurrencyStatus =
getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyStatus).getOrNull()
when (val currency = cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendCoin(
userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch },
coinStatus = cryptoCurrencyStatus,
feeCurrencyStatus = maybeFeeCurrencyStatus,
),
)
}
is CryptoCurrency.Token -> {
sendToken(tokenCurrency = currency, tokenFiatRate = cryptoCurrencyStatus.value.fiatRate)
sendToken(
tokenCurrency = currency,
tokenFiatRate = cryptoCurrencyStatus.value.fiatRate,
feeCurrencyStatus = maybeFeeCurrencyStatus,
)
}
}
}
}
private fun sendToken(tokenCurrency: CryptoCurrency.Token, tokenFiatRate: BigDecimal?) {
private fun sendToken(
tokenCurrency: CryptoCurrency.Token,
tokenFiatRate: BigDecimal?,
feeCurrencyStatus: CryptoCurrencyStatus?,
) {
viewModelScope.launch(dispatchers.io) {
val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }
val maybeCoinStatus = getNetworkCoinStatusUseCase(
@ -394,6 +407,7 @@ internal class TokenDetailsViewModel @Inject constructor(
ifLeft = { null },
ifRight = { it.value.fiatRate },
),
feeCurrencyStatus = feeCurrencyStatus,
),
)
}

View file

@ -110,6 +110,7 @@ internal class WalletViewModel @Inject constructor(
private val unlockWalletsUseCase: UnlockWalletsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
@ -636,9 +637,17 @@ internal class WalletViewModel @Inject constructor(
event = TokenScreenAnalyticsEvent.ButtonSend(coinStatus.currency.symbol),
)
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendCoin(userWallet = userWallet, coinStatus = coinStatus),
)
viewModelScope.launch(dispatchers.main) {
val maybeFeeCurrencyStatus =
getFeePaidCryptoCurrencyStatusSyncUseCase(userWallet.walletId, coinStatus).getOrNull()
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendCoin(
userWallet = userWallet,
coinStatus = coinStatus,
feeCurrencyStatus = maybeFeeCurrencyStatus,
),
)
}
}
override fun onMultiCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
@ -649,21 +658,30 @@ internal class WalletViewModel @Inject constructor(
)
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
uiState = stateFactory.getStateWithClosedBottomSheet()
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendCoin(
userWallet = userWallet,
coinStatus = cryptoCurrencyStatus,
),
)
viewModelScope.launch(dispatchers.main) {
val maybeFeeCurrencyStatus =
getFeePaidCryptoCurrencyStatusSyncUseCase(userWallet.walletId, cryptoCurrencyStatus).getOrNull()
when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
uiState = stateFactory.getStateWithClosedBottomSheet()
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendCoin(
userWallet = userWallet,
coinStatus = cryptoCurrencyStatus,
feeCurrencyStatus = maybeFeeCurrencyStatus,
),
)
}
is CryptoCurrency.Token -> sendToken(userWallet, cryptoCurrencyStatus, maybeFeeCurrencyStatus)
}
is CryptoCurrency.Token -> sendToken(userWallet, cryptoCurrencyStatus)
}
}
private fun sendToken(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus) {
private fun sendToken(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
) {
viewModelScope.launch(dispatchers.io) {
getNetworkCoinStatusUseCase(
userWalletId = userWallet.walletId,
@ -681,6 +699,7 @@ internal class WalletViewModel @Inject constructor(
tokenCurrency = requireNotNull(cryptoCurrencyStatus.currency as? CryptoCurrency.Token),
tokenFiatRate = cryptoCurrencyStatus.value.fiatRate,
coinFiatRate = coinStatus.value.fiatRate,
feeCurrencyStatus = feeCurrencyStatus,
),
)
}

View file

@ -13,10 +13,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
import com.tangem.domain.tokens.RemoveCurrencyUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -76,6 +73,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -91,22 +89,39 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId))
when (val currency = cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> sendCoin(cryptoCurrencyStatus, userWallet)
is CryptoCurrency.Token -> sendToken(currency, cryptoCurrencyStatus.value, userWallet)
viewModelScope.launch(dispatchers.main) {
val maybeFeeCurrencyStatus =
getFeePaidCryptoCurrencyStatusSyncUseCase(userWallet.walletId, cryptoCurrencyStatus).getOrNull()
when (val currency = cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> sendCoin(cryptoCurrencyStatus, userWallet, maybeFeeCurrencyStatus)
is CryptoCurrency.Token -> sendToken(
cryptoCurrency = currency,
cryptoCurrencyStatus = cryptoCurrencyStatus.value,
feeCurrencyStatus = maybeFeeCurrencyStatus,
userWallet = userWallet,
)
}
}
}
private fun sendCoin(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet) {
private fun sendCoin(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
feeCurrencyStatus: CryptoCurrencyStatus?,
) {
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendCoin(userWallet = userWallet, coinStatus = cryptoCurrencyStatus),
action = TradeCryptoAction.New.SendCoin(
userWallet = userWallet,
coinStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
),
)
}
private fun sendToken(
cryptoCurrency: CryptoCurrency.Token,
cryptoCurrencyStatus: CryptoCurrencyStatus.Status,
feeCurrencyStatus: CryptoCurrencyStatus?,
userWallet: UserWallet,
) {
viewModelScope.launch(dispatchers.main) {
@ -125,6 +140,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
tokenCurrency = cryptoCurrency,
tokenFiatRate = cryptoCurrencyStatus.fiatRate,
coinFiatRate = coinStatus.value.fiatRate,
feeCurrencyStatus = feeCurrencyStatus,
),
)
}

View file

@ -85,7 +85,7 @@ spr-client = "3.6.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-453"
tangemBlockchainSdk = "develop-458"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-318"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^