diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 9380922c04..315574334e 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt new file mode 100644 index 0000000000..20dad94e6b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -0,0 +1,251 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.ui.R +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.utils.BigDecimalFormatter +import java.math.BigDecimal + +sealed class NotificationUM(val config: NotificationConfig) { + + open class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonState, + onCloseClick = onCloseClick, + ), + ) { + + data object TotalExceedsBalance : Error( + title = resourceReference(R.string.send_notification_exceed_balance_title), + subtitle = resourceReference(R.string.send_notification_exceed_balance_text), + ) + + data object InvalidAmount : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference(R.string.send_notification_invalid_amount_text), + ) + + data class MinimumAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference( + R.string.send_notification_invalid_minimum_amount_text, + wrappedList(amount, amount), + ), + ) + + data class TransactionLimitError( + val cryptoCurrency: String, + val utxoLimit: String, + val amountLimit: String, + val onConfirmClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.send_notification_transaction_limit_title), + subtitle = resourceReference( + R.string.send_notification_transaction_limit_text, + wrappedList(cryptoCurrency, utxoLimit, amountLimit), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)), + onClick = onConfirmClick, + ), + ) + + data class ExceedsBalance( + val networkIconId: Int, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val mergeFeeNetworkName: Boolean = false, + val onClick: (() -> Unit)? = null, + ) : Error( + title = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_title, + wrappedList(feeName), + ), + subtitle = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), + ), + iconResId = networkIconId, + buttonState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference( + R.string.common_buy_currency, + wrappedList( + if (mergeFeeNetworkName) { + "$currencyName ($feeSymbol)" + } else { + feeName + }, + ), + ), + onClick = onClick, + ) + }, + ) + + data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error( + title = resourceReference(R.string.send_notification_existential_deposit_title), + subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), + onClick = onConfirmClick, + ), + ) + + data class ReserveAmount(val amount: String) : Error( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + wrappedList(amount), + ), + subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), + ) + } + + open class Warning( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.img_attention_20, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ), + ) { + data class HighFeeError( + val currencyName: String, + val amount: String, + val onConfirmClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), + onClick = onConfirmClick, + ), + onCloseClick = onCloseClick, + ) + + data object FeeTooLow : Warning( + title = resourceReference(id = R.string.send_notification_transaction_delay_title), + subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), + ) + + data class TooHigh( + val value: String, + ) : Warning( + title = resourceReference(id = R.string.send_notification_fee_too_high_title), + subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), + ) + + data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onRefresh, + ), + ) + + data class TronAccountNotActivated(val tokenName: String) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference( + R.string.send_tron_account_activation_error, + wrappedList(tokenName), + ), + ) + + data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( + title = resourceReference(R.string.send_network_fee_warning_title), + subtitle = resourceReference( + R.string.common_network_fee_warning_content, + wrappedList(cryptoAmount, fiatAmount), + ), + ) + } + + sealed interface Cardano { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + ) + + data object InsufficientBalanceToTransferCoin : Error( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + ) + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Error( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + ) + } + + sealed interface Koinos { + data class InsufficientRecoverableMana( + val mana: BigDecimal, + val maxMana: BigDecimal, + ) : Error( + title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title), + subtitle = resourceReference( + R.string.koinos_insufficient_mana_to_send_koin_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()), + BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()), + ), + ), + ) + + data object InsufficientBalance : Error( + title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title), + subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description), + ) + + data class ManaExceedsBalance( + val availableKoinForTransfer: BigDecimal, + val onReduceClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title), + subtitle = resourceReference( + R.string.koinos_mana_exceeds_koin_balance_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmount( + availableKoinForTransfer, + Blockchain.Koinos.currency, + Blockchain.Koinos.decimals(), + ), + ), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)), + onClick = onReduceClick, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt new file mode 100644 index 0000000000..526007b989 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -0,0 +1,362 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.error.GetFeeError +import java.math.BigDecimal + +@Suppress("LargeClass") +object NotificationsFactory { + + fun MutableList.addFeeUnreachableNotification( + feeError: GetFeeError, + tokenName: String, + onReload: () -> Unit, + ) { + when (feeError) { + is GetFeeError.BlockchainErrors.TronActivationError -> add( + NotificationUM.Warning.TronAccountNotActivated(tokenName), + ) + is GetFeeError.DataError, + is GetFeeError.UnknownError, + -> add( + NotificationUM.Warning.NetworkFeeUnreachable(onReload), + ) + else -> { + /* do nothing */ + } + } + } + + fun MutableList.addExceedBalanceNotification( + feeAmount: BigDecimal, + sendingAmount: BigDecimal, + isSubtractionAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + + if (!isSubtractionAvailable) return + + val showNotification = sendingAmount + feeAmount > balance + if (showNotification) { + add(NotificationUM.Error.TotalExceedsBalance) + } + } + + fun MutableList.addReserveAmountErrorNotification( + reserveAmount: BigDecimal?, + sendingAmount: BigDecimal, + cryptoCurrency: CryptoCurrency, + isAccountFunded: Boolean, + ) { + if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) { + add( + NotificationUM.Error.ReserveAmount( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } + + fun MutableList.addTransactionLimitErrorNotification( + utxoLimit: UtxoAmountLimit?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + if (utxoLimit != null) { + add( + NotificationUM.Error.TransactionLimitError( + cryptoCurrency = cryptoCurrency.name, + utxoLimit = utxoLimit.maxLimit.toPlainString(), + amountLimit = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = utxoLimit.maxAmount, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + utxoLimit.maxAmount, + NotificationUM.Error.TransactionLimitError::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addExistentialWarningNotification( + existentialDeposit: BigDecimal?, + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + onReduceClick: ( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) -> Unit, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val balance = cryptoCurrencyStatus.value.amount ?: return + val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { + feeAmount + } else { + receivedAmount + } + val diff = balance.minus(spendingAmount) + if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) { + add( + NotificationUM.Error.ExistentialDeposit( + deposit = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = existentialDeposit, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + existentialDeposit, + existentialDeposit.minus(diff), + NotificationUM.Error.ExistentialDeposit::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addFeeCoverageNotification( + isFeeCoverage: Boolean, + amountField: AmountFieldModel, + sendingValue: BigDecimal, + appCurrency: AppCurrency, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val amountValue = amountField.cryptoAmount.value ?: return + + val cryptoDiff = amountValue.minus(sendingValue) + if (isFeeCoverage) { + add( + NotificationUM.Warning.FeeCoverageNotification( + cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = cryptoDiff, + cryptoCurrency = cryptoCurrency, + ), + fiatAmount = getFiatString( + value = cryptoDiff, + rate = fiatRate, + appCurrency = appCurrency, + ), + ), + ) + } + } + + fun MutableList.addDustWarningNotification( + dustValue: BigDecimal?, + feeValue: BigDecimal, + sendingAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ) { + if (dustValue == null) return + val isExceedsLimit = checkDustLimits( + feeAmount = feeValue, + receivedAmount = sendingAmount, + dustValue = dustValue, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + ) + if (isExceedsLimit) { + add( + NotificationUM.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + ), + ) + } + } + + fun MutableList.addExceedsBalanceNotification( + cryptoCurrencyWarning: CryptoCurrencyWarning?, + cryptoCurrencyStatus: CryptoCurrencyStatus, + shouldMergeFeeNetworkName: Boolean, + onClick: (CryptoCurrency) -> Unit, + onAnalyticsEvent: (CryptoCurrency) -> Unit, + ) { + when (cryptoCurrencyWarning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId, + networkName = cryptoCurrencyWarning.coinCurrency.name, + currencyName = cryptoCurrencyStatus.currency.name, + feeName = cryptoCurrencyWarning.coinCurrency.name, + feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + onClick(cryptoCurrencyWarning.coinCurrency) + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyStatus.currency) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = cryptoCurrencyWarning.feeCurrency + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, + currencyName = cryptoCurrencyWarning.currency.name, + feeName = cryptoCurrencyWarning.feeCurrencyName, + feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol, + networkName = cryptoCurrencyWarning.networkName, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + currency?.let { + onClick(currency) + } + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyWarning.currency) + } + else -> Unit + } + } + + fun MutableList.addValidateTransactionNotifications( + dustValue: BigDecimal, + fee: Fee?, + validationError: Throwable?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (validationError) { + is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError( + error = validationError, + sendingCurrency = cryptoCurrency, + dustValue = dustValue, + ) + is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError( + error = validationError, + onReduceClick = onReduceClick, + ) + null -> (fee as? Fee.CardanoToken)?.let { + add( + NotificationUM.Cardano.MinAdaValueCharged( + tokenName = cryptoCurrency.name, + minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals), + ), + ) + } + else -> return + } + } + + private fun MutableList.addCardanoTransactionValidationError( + error: BlockchainSdkError.Cardano, + sendingCurrency: CryptoCurrency, + dustValue: BigDecimal?, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (sendingCurrency) { + is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name) + } + }.let(::add) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + dustValue?.let { + add( + NotificationUM.Error.MinimumAmountError( + amount = it.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + } + } + } + + private fun MutableList.addKoinosTransactionValidationError( + error: BlockchainSdkError.Koinos, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (error) { + is BlockchainSdkError.Koinos.InsufficientBalance -> { + add(NotificationUM.Koinos.InsufficientBalance) + } + is BlockchainSdkError.Koinos.InsufficientMana -> { + add( + NotificationUM.Koinos.InsufficientRecoverableMana( + mana = error.manaBalance ?: BigDecimal.ZERO, + maxMana = error.maxMana ?: BigDecimal.ZERO, + ), + ) + } + is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> { + add( + NotificationUM.Koinos.ManaExceedsBalance( + availableKoinForTransfer = error.availableKoinForTransfer, + onReduceClick = { + onReduceClick( + error.availableKoinForTransfer, + NotificationUM.Koinos.InsufficientRecoverableMana::class.java, + ) + }, + ), + ) + } + else -> {} + } + } + + private fun checkDustLimits( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + dustValue: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + val change = when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + balance - (feeAmount + receivedAmount) + } + is CryptoCurrency.Token -> { + val balance = feeCurrencyStatus?.value?.amount ?: BigDecimal.ZERO + balance - feeAmount + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + return receivedAmount < dustValue || isChangeLowerThanDust + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt similarity index 100% rename from domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt rename to domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index a9dcad67cf..dca4eefe62 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.CardWithIcon import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference @@ -13,7 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingNotification import kotlinx.collections.immutable.ImmutableList @Composable -internal fun NotificationsBlock(notifications: ImmutableList) { +internal fun NotificationsBlock(notifications: ImmutableList) { notifications.forEach { notification -> key(notification) { if (notification is StakingNotification.Warning.TransactionInProgress) { @@ -32,8 +33,15 @@ internal fun NotificationsBlock(notifications: ImmutableList TangemTheme.colors.icon.warning is StakingNotification.Warning -> TangemTheme.colors.icon.accent + + is NotificationUM.Error.ExceedsBalance, + is NotificationUM.Warning, + -> null + + is StakingNotification.Error, + is NotificationUM.Error, + -> TangemTheme.colors.icon.warning }, ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 830677086d..89f796a6b7 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -56,6 +56,11 @@ object BlockchainUtils { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } + fun isArbitrum(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Arbitrum + } + data class BlockchainInfo( val blockchainId: String, val name: String,