Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-01 15:24:59 +07:00
parent ab2f0f58bb
commit 22ad2ddc67
22 changed files with 190 additions and 15 deletions

View file

@ -79,6 +79,18 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideOpenTrustlineUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
): OpenTrustlineUseCase {
return OpenTrustlineUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@Singleton
fun provideDismissIncompleteTransactionUseCase(

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
@Suppress("CyclomaticComplexMethod")
fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
return when (val unavailabilityReason = this) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
@ -46,6 +47,9 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
id = R.string.warning_receive_blocked_hedera_token_association_required_message,
)
ScenarioUnavailabilityReason.TrustlineRequired -> resourceReference(
id = R.string.warning_receive_blocked_token_trustline_required_message,
)
ScenarioUnavailabilityReason.UsedOutdatedData -> {
resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance)
}

View file

@ -1299,8 +1299,8 @@
<string name="warning_token_balance_not_updated">Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite.</string>
<string name="warning_token_required_min_coin_reserve">Nicht genug %1$s. Lade Dein XLM-Konto auf, um dieses Token zu verknüpfen</string>
<string name="warning_token_trustline_button_title">Trustline aktivieren</string>
<string name="warning_token_trustline_subtiile">Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s.</string>
<string name="warning_token_trustline_tiile">Trustline erforderlich</string>
<string name="warning_token_trustline_subtitle">Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s.</string>
<string name="warning_token_trustline_title">Trustline erforderlich</string>
<string name="wc_alert_audit_unknown_domain">Unbekannte Domäne</string>
<string name="wc_alert_connect_anyway">Trotzdem verbinden</string>
<string name="wc_alert_connection_timeout_description">Zeitüberschreitungsfehler. Bitte versuche es später erneut.</string>

View file

@ -1279,8 +1279,8 @@
<string name="warning_token_balance_not_updated">残高が古い可能性があります。ページを更新してください。</string>
<string name="warning_token_required_min_coin_reserve">%1$sが足りません。このトークンを関連付けるには、XLMアカウントにチャージしてください。</string>
<string name="warning_token_trustline_button_title">トラストラインを有効にする</string>
<string name="warning_token_trustline_subtiile">このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには%1$s %2$s予備金が必要です。</string>
<string name="warning_token_trustline_tiile">トラストラインが必要</string>
<string name="warning_token_trustline_subtitle">このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには%1$s %2$s予備金が必要です。</string>
<string name="warning_token_trustline_title">トラストラインが必要</string>
<string name="wc_alert_audit_unknown_domain">不明なドメイン</string>
<string name="wc_alert_connect_anyway">とにかく接続する</string>
<string name="wc_alert_connection_timeout_description">タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。</string>

View file

@ -1263,8 +1263,8 @@
<string name="warning_testnet_card_title">Только для целей тестирования</string>
<string name="warning_token_balance_not_updated">Возможно, баланс устарел. Обновите страницу.</string>
<string name="warning_token_trustline_button_title">Открыть Trustline</string>
<string name="warning_token_trustline_subtiile">Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив.</string>
<string name="warning_token_trustline_tiile">Откройте Trustline</string>
<string name="warning_token_trustline_subtitle">Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив.</string>
<string name="warning_token_trustline_title">Откройте Trustline</string>
<string name="wc_alert_session_disconnected_description">Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect.</string>
<string name="wc_alert_wrong_card_description">Выбрана не верная карта или кольцо</string>
<string name="wc_common_address">Адрес</string>

View file

@ -1344,8 +1344,8 @@
<string name="warning_token_balance_not_updated">Balance may be outdated. Refresh the page.</string>
<string name="warning_token_required_min_coin_reserve">Not enough %1$s. Top up your XLM account to associate this token</string>
<string name="warning_token_trustline_button_title">Enable Trustline</string>
<string name="warning_token_trustline_subtiile">A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve.</string>
<string name="warning_token_trustline_tiile">Trustline Required</string>
<string name="warning_token_trustline_subtitle">A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve.</string>
<string name="warning_token_trustline_title">Trustline Required</string>
<string name="wc_alert_audit_unknown_domain">Unknown domain</string>
<string name="wc_alert_connect_anyway">Connect anyway</string>
<string name="wc_alert_connection_timeout_description">Timeout error. Please, try again later.</string>

View file

@ -57,6 +57,7 @@ internal class DefaultCustomTokensRepository(
-> true
Blockchain.Cardano,
Blockchain.Sui,
Blockchain.Stellar,
-> blockchain.validateContractAddress(contractAddress)
else -> blockchain.validateAddress(contractAddress)
}

View file

@ -8,7 +8,11 @@ internal class SdkRequirementsConditionConverter : Converter<SdkRequirementsCond
override fun convert(value: SdkRequirementsCondition): AssetRequirementsCondition {
return when (value) {
is SdkRequirementsCondition.PaidTransaction -> AssetRequirementsCondition.PaidTransaction
is SdkRequirementsCondition.RequiredTrustline -> TODO("added in [REDACTED_TASK_KEY]")
is SdkRequirementsCondition.RequiredTrustline -> AssetRequirementsCondition.RequiredTrustline(
requiredAmount = requireNotNull(value.amount.value),
currencySymbol = value.amount.currencySymbol,
decimals = value.amount.decimals,
)
is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee(
feeAmount = requireNotNull(value.feeAmount.value),
feeCurrencySymbol = value.feeAmount.currencySymbol,

View file

@ -54,6 +54,8 @@ sealed class ScenarioUnavailabilityReason {
data object UnassociatedAsset : ScenarioUnavailabilityReason()
data object TrustlineRequired : ScenarioUnavailabilityReason()
enum class WithdrawalScenario {
SELL, SEND // TODO staking create&process STAKING
}

View file

@ -167,6 +167,7 @@ sealed class TokenScreenAnalyticsEvent(
private const val SINGLE_WALLET = "Single Wallet"
private const val LOADING = "Loading"
private const val ASSET_REQUIREMENT = "AssetRequirement"
private const val TRUSTLINE_REQUIREMENT = "TrustlineRequirement"
@Suppress("CyclomaticComplexMethod")
fun ScenarioUnavailabilityReason.toReasonAnalyticsText(): String {
@ -177,6 +178,7 @@ sealed class TokenScreenAnalyticsEvent(
is ScenarioUnavailabilityReason.StakingUnavailable,
-> UNAVAILABLE
ScenarioUnavailabilityReason.UnassociatedAsset -> ASSET_REQUIREMENT
ScenarioUnavailabilityReason.TrustlineRequired -> TRUSTLINE_REQUIREMENT
is ScenarioUnavailabilityReason.EmptyBalance -> EMPTY
is ScenarioUnavailabilityReason.PendingTransaction -> PENDING
ScenarioUnavailabilityReason.UsedOutdatedData -> CACHING

View file

@ -61,4 +61,11 @@ sealed class CryptoCurrencyWarning {
) : CryptoCurrencyWarning()
data object UsedOutdatedDataWarning : CryptoCurrencyWarning()
data class RequiredTrustline(
val currency: CryptoCurrency,
val currencySymbol: String,
val requiredAmount: BigDecimal,
val currencyDecimals: Int,
) : CryptoCurrencyWarning()
}

View file

@ -375,12 +375,14 @@ class GetCryptoCurrencyActionsUseCase(
}
private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason {
return if (requirements is AssetRequirementsCondition.PaidTransaction ||
requirements is AssetRequirementsCondition.PaidTransactionWithFee
) {
ScenarioUnavailabilityReason.UnassociatedAsset
} else {
ScenarioUnavailabilityReason.None
return when (requirements) {
AssetRequirementsCondition.PaidTransaction,
is AssetRequirementsCondition.PaidTransactionWithFee,
-> ScenarioUnavailabilityReason.UnassociatedAsset
is AssetRequirementsCondition.IncompleteTransaction,
null,
-> ScenarioUnavailabilityReason.None
is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired
}
}

View file

@ -222,6 +222,12 @@ class GetCurrencyWarningsUseCase(
): CryptoCurrencyWarning? {
return when (val requirements = walletManagersFacade.getAssetRequirements(userWalletId, currency)) {
is AssetRequirementsCondition.PaidTransaction -> HederaWarnings.AssociateWarning(currency = currency)
is AssetRequirementsCondition.RequiredTrustline -> CryptoCurrencyWarning.RequiredTrustline(
currency = currency,
requiredAmount = requirements.requiredAmount,
currencyDecimals = requirements.decimals,
currencySymbol = requirements.currencySymbol,
)
is AssetRequirementsCondition.PaidTransactionWithFee -> {
HederaWarnings.AssociateWarningWithFee(
currency = currency,

View file

@ -9,6 +9,15 @@ sealed class AssetRequirementsCondition {
*/
data object PaidTransaction : AssetRequirementsCondition()
/**
* Trustlines are an explicit opt-in for an account to hold a particular asset.
*/
data class RequiredTrustline(
val requiredAmount: BigDecimal,
val currencySymbol: String,
val decimals: Int,
) : AssetRequirementsCondition()
/**
* The exact value of the fee for this type of condition is stored in `feeAmount`.
*/

View file

@ -0,0 +1,10 @@
package com.tangem.domain.transaction.error
import java.math.BigDecimal
sealed interface OpenTrustlineError {
val message: String?
data class SomeError(override val message: String?) : OpenTrustlineError
data class NotEnoughCoin(val amount: BigDecimal, override val message: String?) : OpenTrustlineError
}

View file

@ -0,0 +1,44 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.transaction.error.OpenTrustlineError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
class OpenTrustlineUseCase(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Either<OpenTrustlineError, Unit> {
return either {
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = null,
twinKey = null, // use null here because no assets support for Twin cards
)
catch(
block = {
when (val result = walletManagersFacade.fulfillRequirements(userWalletId, currency, signer)) {
is SimpleResult.Failure -> when (val error = result.error) {
is BlockchainSdkError.Stellar.MinReserveRequired ->
raise(OpenTrustlineError.NotEnoughCoin(error.amount, result.error.customMessage))
else -> raise(OpenTrustlineError.SomeError(result.error.customMessage))
}
SimpleResult.Success -> Unit
}
},
catch = { error -> raise(OpenTrustlineError.SomeError(error.message)) },
)
}
}
}

View file

@ -51,6 +51,7 @@ internal class TokenDetailsNotificationsAnalyticsSender(
is TokenDetailsNotification.RentInfo,
is TokenDetailsNotification.NetworkShutdown,
is TokenDetailsNotification.HederaAssociateWarning,
is TokenDetailsNotification.RequiredTrustlineWarning,
is TokenDetailsNotification.KoinosMana,
is TokenDetailsNotification.MigrationMaticToPol,
is TokenDetailsNotification.UsedOutdatedData,

View file

@ -62,6 +62,8 @@ interface TokenDetailsClickIntents {
fun onRetryIncompleteTransactionClick()
fun onOpenTrustlineClick()
fun onDismissIncompleteTransactionClick()
fun onConfirmDismissIncompleteTransactionClick()

View file

@ -56,9 +56,11 @@ import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Compan
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.transaction.error.IncompleteTransactionError
import com.tangem.domain.transaction.error.OpenTrustlineError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.AssociateAssetUseCase
import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase
import com.tangem.domain.transaction.usecase.OpenTrustlineUseCase
import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
@ -116,6 +118,7 @@ internal class TokenDetailsModel @Inject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val associateAssetUseCase: AssociateAssetUseCase,
private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase,
private val openTrustlineUseCase: OpenTrustlineUseCase,
private val dismissIncompleteTransactionUseCase: DismissIncompleteTransactionUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val analyticsEventsHandler: AnalyticsEventHandler,
@ -893,6 +896,37 @@ internal class TokenDetailsModel @Inject constructor(
}
}
override fun onOpenTrustlineClick() {
analyticsEventsHandler.send(
TokenScreenAnalyticsEvent.Associate(
tokenSymbol = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
modelScope.launch(dispatchers.mainImmediate) {
openTrustlineUseCase(
userWalletId = userWalletId,
currency = cryptoCurrency,
).fold(
ifLeft = { e ->
Timber.e(e.message)
internalUiState.value = when (e) {
is OpenTrustlineError.SomeError ->
stateFactory.getStateWithErrorDialog(stringReference(e.message.orEmpty()))
is OpenTrustlineError.NotEnoughCoin -> {
val text = resourceReference(
id = R.string.warning_token_required_min_coin_reserve,
formatArgs = wrappedList(e.amount),
)
stateFactory.getStateWithErrorDialog(text)
}
}
},
ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() },
)
}
}
override fun onDismissIncompleteTransactionClick() {
analyticsEventsHandler.send(
TokenScreenAnalyticsEvent.RevealCancel(

View file

@ -193,6 +193,24 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
),
)
data class RequiredTrustlineWarning(
private val currency: CryptoCurrency,
private val amount: String,
private val currencySymbol: String,
private val onOpenClick: () -> Unit,
) : Warning(
title = resourceReference(id = R.string.warning_token_trustline_title),
subtitle = resourceReference(
id = R.string.warning_token_trustline_subtitle,
formatArgs = wrappedList(amount, currencySymbol),
),
iconResId = currency.networkIconResId,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_token_trustline_button_title),
onClick = onOpenClick,
),
)
data class KaspaIncompleteTransactionWarning(
private val currency: CryptoCurrency,
private val amount: String,

View file

@ -42,6 +42,12 @@ internal class TokenDetailsNotificationConverter(
return newNotifications.toImmutableList()
}
fun removeRequiredTrustlineWarning(currentState: TokenDetailsState): ImmutableList<TokenDetailsNotification> {
val newNotifications = currentState.notifications.toMutableList()
newNotifications.removeBy { it is RequiredTrustlineWarning }
return newNotifications.toImmutableList()
}
fun removeKaspaIncompleteTransactionWarning(
currentState: TokenDetailsState,
): ImmutableList<TokenDetailsNotification> {
@ -118,6 +124,12 @@ internal class TokenDetailsNotificationConverter(
feeCurrencySymbol = warning.feeCurrencySymbol,
onAssociateClick = clickIntents::onAssociateClick,
)
is CryptoCurrencyWarning.RequiredTrustline -> RequiredTrustlineWarning(
currency = warning.currency,
amount = warning.requiredAmount.format { crypto(symbol = "", warning.currencyDecimals) },
currencySymbol = warning.currencySymbol,
onOpenClick = clickIntents::onOpenTrustlineClick,
)
is KaspaWarnings.IncompleteTransaction -> KaspaIncompleteTransactionWarning(
currency = warning.currency,
amount = warning.amount.format { crypto(symbol = "", decimals = warning.currencyDecimals) },

View file

@ -334,6 +334,11 @@ internal class TokenDetailsStateFactory(
return state.copy(notifications = notificationConverter.removeHederaAssociateWarning(state))
}
fun getStateWithRemovedRequiredTrustlineNotification(): TokenDetailsState {
val state = currentStateProvider()
return state.copy(notifications = notificationConverter.removeRequiredTrustlineWarning(state))
}
fun getStateWithRemovedKaspaIncompleteTransactionNotification(): TokenDetailsState {
val state = currentStateProvider()
return state.copy(