Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-15 13:26:18 +04:00
parent 4ee114e3cd
commit 50dca50508
12 changed files with 460 additions and 8 deletions

View file

@ -301,6 +301,12 @@ sealed class NotificationUM(val config: NotificationConfig) {
),
subtitle = TextReference.EMPTY,
)
data object YieldSupplyHighNetworkFee : Info(
title = resourceReference(id = R.string.yield_module_high_network_fees_notification_title),
subtitle = resourceReference(id = R.string.yield_module_high_network_fees_notification_description),
iconTint = NotificationConfig.IconTint.Accent,
)
}
sealed interface Cardano {

View file

@ -849,6 +849,9 @@
<string name="no_trustline_xlm_asset">Das Zielkonto verfügt nicht über eine Vertrauensstellung für das gesendete Asset.</string>
<string name="notification_black_friday_text">Sicher Dir 10 $ in BTC mit jeder Wallet greif schnell zu!</string>
<string name="notification_black_friday_title">Black Friday: bis zu 30% Nachlass</string>
<string name="notification_one_plus_one_button">Los geht\'s</string>
<string name="notification_one_plus_one_text">Nur für kurze Zeit!</string>
<string name="notification_one_plus_one_title">1+1: Beim Kauf einer Wallet erhältst Du 50% Rabatt</string>
<string name="notification_referral_promo_button">Jetzt beitreten</string>
<string name="notification_referral_promo_text">Teile Deinen Code verdiene 5 USDT pro Verkauf. Deine Freunde erhalten 10 % Rabatt.</string>
<string name="notification_referral_promo_title">Erhalte BELOHNUNGEN für jeden Freund!</string>

View file

@ -848,8 +848,8 @@
<string name="notification_black_friday_text">ウォレットごとに$10相当のBTCをプレゼント\nお早めに</string>
<string name="notification_black_friday_title">ブラックフライデー最大30% オフ</string>
<string name="notification_one_plus_one_button">今すぐチェック</string>
<string name="notification_one_plus_one_text">理想のTangemセットを揃えよう。期間限定。</string>
<string name="notification_one_plus_one_title">111つ購入で、2つ目が50%オフ</string>
<string name="notification_one_plus_one_text">期間限定!</string>
<string name="notification_one_plus_one_title">11ウォレット1つ購入で、2つ目が50%オフ</string>
<string name="notification_referral_promo_button">今すぐ参加</string>
<string name="notification_referral_promo_text">コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。</string>
<string name="notification_referral_promo_title">友達への紹介で報酬を獲得しよう!</string>
@ -1391,7 +1391,7 @@
<string name="tangem_pay_explore_transaction">取引を表示</string>
<string name="tangem_pay_fee_subtitle">サービス手数料</string>
<string name="tangem_pay_fee_title">手数料</string>
<string name="tangem_pay_freeze_card_alert_body">カードを紛失したり盗まれたりしても、資金を安全に保てます。一時停止はいつでも解除できます。</string>
<string name="tangem_pay_freeze_card_alert_body">資金を安全に保管します。凍結はいつでも解除できます。</string>
<string name="tangem_pay_freeze_card_alert_title">カードを一時停止しますか?</string>
<string name="tangem_pay_freeze_card_failed">カードを凍結できませんでした。しばらくしてからもう一度お試しください。</string>
<string name="tangem_pay_freeze_card_freeze">一時停止</string>

View file

@ -16,8 +16,10 @@
<string name="access_code_navtitle">Код доступа</string>
<string name="account_add_limit_dialog_description">Превышен лимит %1$s аккаунтов. Архивируйте, чтобы создать новый.</string>
<string name="account_add_limit_dialog_title">Невозможно добавить новый аккаунт.</string>
<string name="account_archive_success_message">Аккаунт заархивирован</string>
<string name="account_archived_accounts">Архивированные аккаунты</string>
<string name="account_archived_recover">Восстановить</string>
<string name="account_archived_recover_dialog_description">Вы собираетесь восстановить \"%1$s\".</string>
<string name="account_archived_recover_dialog_title">Восстановить аккаунт</string>
<string name="account_archived_recover_error_message">Лимит в 20 активных аккаунтов достигнут. Пожалуйста, зархивируйте один аккаунт, чтобы продолжить.</string>
<string name="account_archived_recover_error_title">Невозможно восстановить аккаунт</string>
@ -26,12 +28,14 @@
<string name="account_could_not_archive_referral_program_message">Этот аккаунт участвует в реферальной программе</string>
<string name="account_could_not_archive_referral_program_title">Этот аккаунт не может быть архивирован.</string>
<string name="account_could_not_create">Мы не смогли создать аккаунт. Пожалуйста, попробуйте позже.</string>
<string name="account_create_success_message">Аккаунт создан</string>
<string name="account_details_archive">Архивный аккаунт</string>
<string name="account_details_archive_action">Архив</string>
<string name="account_details_archive_description">Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно</string>
<string name="account_details_archiving">Архивация...</string>
<string name="account_details_title">Аккаунт</string>
<string name="account_edit_failure_dialog_title">Невозможно отредактировать аккаунт</string>
<string name="account_edit_success_message">Аккаунт сохранен</string>
<string name="account_for_rewards">Аккаунт для наград</string>
<string name="account_form_create_button">Добавить аккаунт</string>
<string name="account_form_edit_button">Сохранить</string>
@ -46,6 +50,8 @@
<string name="account_main_account_title">Основной аккаунт</string>
<string name="account_recover_limit_dialog_description">Лимит %1$s активных аккаунтов превышен. Архивируйте один, чтобы продолжить.</string>
<string name="account_recover_limit_dialog_title">Невозможно восстановить аккаунт</string>
<string name="account_recover_success_message">Аккаунт восстановлен</string>
<string name="account_reorder_description">Нажмите и удерживайте аккаунт, чтобы изменить порядок аккаунтов.</string>
<string name="account_unsaved_dialog_action_first">Продолжить</string>
<string name="account_unsaved_dialog_action_second">Отменить</string>
<string name="account_unsaved_dialog_message_create">Вы уверены, что хотите создание нового аккаунта?</string>
@ -87,14 +93,17 @@
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой или кольцом из-за ограничений прошивки.</string>
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты или кольца?</string>
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
<string name="app_settings_access_code_warning">Сначала установите код доступа, чтобы включить биометрию.</string>
<string name="app_settings_biometrics_footer">Используйте %1$s, чтобы быстро и безопасно разблокировать кошелёк и выполнять чувствительные действия, например, подписывать транзакции. Для аппаратных кошельков всё ещё требуется карта или кольцо для подписи.</string>
<string name="app_settings_default_fee">Комиссия по-умолчанию</string>
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</string>
<string name="app_settings_enable_biometrics_description">Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem</string>
<string name="app_settings_enable_biometrics_title">Включите биометрическую аутентификацию</string>
<string name="app_settings_off_biometrics_alert_message">Отключение %1$s потребует ввода вашего кода доступа для разблокировки приложения и работы с кошельком.</string>
<string name="app_settings_off_require_access_code_alert_message">Позже будет необходимо ввести код доступа к вашему кошельку, чтобы мы могли безопасно сохранить его для будущего использования.</string>
<string name="app_settings_off_saved_access_code_alert_message">Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком.</string>
<string name="app_settings_off_saved_wallet_alert_message">При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения.</string>
<string name="app_settings_on_require_access_code_alert_message">Это удалит все сохранённые коды доступа к кошелькам. Любое дальнейшее взаимодействие с кошельком будет требовать ввода кода доступа.</string>
<string name="app_settings_require_access_code">Требуется код доступа</string>
<string name="app_settings_require_access_code_footer">Эта опция отключает использование биометрии для выполнения чувствительных действий. Каждый раз, например при подписании транзакции, вам потребуется вводить код доступа.</string>
<string name="app_settings_saved_access_codes">Сохранение кода доступа</string>
@ -144,6 +153,7 @@
<string name="biometric_lockout_warning_description_2">Вход по биометрии временно заблокирован. Попробуйте снова через 30 секунд или разблокируйте кошелёк с помощью прикладывания устройства или кода доступа.</string>
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
<string name="biometric_updated_warning_description">Биометрические данные на вашем устройстве были обновлены. Пожалуйста, выберите свой кошелёк и введите его код доступа, чтобы повторно включить вход по биометрии.</string>
<string name="biometric_updated_warning_title">Внимание</string>
<string name="bitcoin_promo_activation_error">При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже.</string>
<string name="bitcoin_promo_activation_error_title">Ошибка активации</string>
@ -299,6 +309,7 @@
<string name="common_later">Позже</string>
<string name="common_learn_more">Узнать больше</string>
<string name="common_left">Осталось %1$s</string>
<string name="common_legacy_bitcoin_address">Легаси Bitcoin</string>
<string name="common_locked">Заблокирован</string>
<string name="common_locked_wallets">Заблокированные кошельки</string>
<string name="common_main_network">Основная сеть</string>
@ -311,6 +322,8 @@
<item quantity="many">%d сетей</item>
<item quantity="other">%d сети</item>
</plurals>
<string name="common_new_address">Новый адрес</string>
<string name="common_news">Новости</string>
<string name="common_next">Далее</string>
<string name="common_nft">NFT</string>
<string name="common_no">Нет</string>
@ -363,6 +376,7 @@
<string name="common_support">Поддержка</string>
<string name="common_supported_networks">Поддерживаемые сети</string>
<string name="common_swap">Обменять</string>
<string name="common_tangem">Tangem</string>
<string name="common_terms_and_conditions">условия участия</string>
<string name="common_terms_of_use">Условиями использования</string>
<string name="common_to">На</string>
@ -391,6 +405,7 @@
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="currency_subtitle_expanded">Доступные сети</string>
<string name="custom_token_another_account_dialog_description">Деривация вашего токена совпадает с деривацией %1$s. Токен будет добавлен к этой учётной записи.</string>
<string name="custom_token_another_account_dialog_title">Деривация принадлежит другому аккаунту</string>
<string name="custom_token_another_account_snackbar_text">Токен добавлен в аккаунт %1$s</string>
<string name="custom_token_contract_address_input_title">Адрес контракта</string>
<string name="custom_token_creation_error_invalid_contract_address">Адрес контракта некорректен</string>
@ -527,6 +542,8 @@
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">ID транзакции скопирован</string>
<string name="exсhange_token_description">Обменяйте любой актив в вашем портфеле на этот токен</string>
<string name="feed_market_and_news">Рынок и Новости</string>
<string name="feed_trending_now">В тренде</string>
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
<string name="feedback_preface_scan_failed">Скажите, пожалуйста, какая у вас карта или кольцо?</string>
@ -687,6 +704,7 @@
<string name="markets_apy_placeholder">APY %s</string>
<string name="markets_common_my_portfolio">Мой портфель</string>
<string name="markets_common_title">Рынок</string>
<string name="markets_earn_common_title">Зарабатывай с Tangem</string>
<string name="markets_generate_addresses_notification">Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту или кольцо Tangem кошелька</string>
<string name="markets_hint">Потяните вверх или коснитесь поисковой строки, чтобы добавить токен</string>
<string name="markets_insights_info_description_message">Данные раздела получены из следующих сетей: %s</string>
@ -791,6 +809,8 @@
<string name="markets_yield_supply_banner_title">Активировать режим доходности</string>
<string name="mobile_wallet_requires_min_os_warning_body">Обновитесь до версии %1$s, чтобы создать мобильный кошелёк</string>
<string name="mobile_wallet_requires_min_os_warning_title">Мобильный кошелек требует %1$s или новее.</string>
<string name="news_all_news">Все новости</string>
<string name="news_stay_in_the_loop">Будьте в курсе</string>
<string name="nfc_error_unavailable">Функция NFC недоступна на вашем устройстве</string>
<string name="nft_about_title">О NFT</string>
<string name="nft_asset">NFT</string>
@ -1198,6 +1218,7 @@
<string name="send_tron_account_activation_error">%1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес.</string>
<string name="send_validation_amount_exceeds_balance">Сумма превышает остаток</string>
<string name="send_validation_destination_tag_required_description">Для завершения транзакции на указанный адрес необходим тег назначения (мемо).</string>
<string name="send_validation_destination_tag_required_title">Требуется Destination Tag</string>
<string name="send_validation_invalid_amount">Недопустимая сумма</string>
<string name="send_validation_invalid_fee">Комиссия превышает остаток</string>
<string name="send_validation_invalid_total">Отправляемая сумма превышает остаток</string>
@ -1225,6 +1246,8 @@
<string name="staking_account_initialization_footer">Сетевая комиссия — это небольшая плата за обработку и подтверждение вашей транзакции в блокчейне.</string>
<string name="staking_account_initialization_message">Чтобы начать стейкинг, ваш TON-аккаунт должен быть активирован транзакцией самому себе на 1 TON. Средства остаются в вашем кошельке — этот шаг лишь активирует ваш аккаунт для участия в стейкинге.</string>
<string name="staking_account_initialization_title">Активация аккаунта</string>
<string name="staking_alert_network_fee_updated_message">Сетевая комиссия изменилась. Пожалуйста, проверьте новую сумму перед продолжением.</string>
<string name="staking_alert_network_fee_updated_title">Комиссия сети обновлена</string>
<string name="staking_amount_requirement_error">Сумма для стейкинга должна быть не менее %s</string>
<string name="staking_amount_tron_integer_error">Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX.</string>
<string name="staking_amount_tron_integer_error_unstaking">Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети.</string>
@ -1263,6 +1286,7 @@
<string name="staking_give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для стейкинга.</string>
<string name="staking_legal">Пользуясь стейкинг сервисом, вы соглашаетесь с %1$s и %2$s</string>
<string name="staking_locked">Заблокировано</string>
<string name="staking_max_amount_requirement_error">Максимальная сумму: %s</string>
<string name="staking_migrate">Переместить</string>
<string name="staking_native">Нативный стейкинг</string>
<string name="staking_no_validators_error_message">В данный момент нет доступных активных валидаторов для стейкинга. Пожалуйста, попробуйте позже.</string>
@ -1598,6 +1622,7 @@
<string name="visa_onboarding_access_code_navigation_title">Код доступа</string>
<string name="visa_onboarding_account_activation_navigation_title">Активация аккаунта</string>
<string name="visa_onboarding_pin_not_accepted">ПИН не принят. Попробуйте ещё раз или введите другой код.</string>
<string name="visa_unlock_notification_button">Разблокировать</string>
<string name="wallet_add_common_title">Выберите способ добавления кошелька</string>
<string name="wallet_add_hardware_description">Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька.</string>
<string name="wallet_add_hardware_info_create">Создать аппаратный кошелёк</string>
@ -1934,6 +1959,7 @@
<string name="yield_module_fee_policy_sheet_title">Политика комиссий за пополнение</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода.</string>
<string name="yield_module_high_fee_error">Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы.</string>
<string name="yield_module_high_network_fees_notification_title">Высокая сетевая комиссия</string>
<string name="yield_module_historical_returns">Историческая доходность</string>
<string name="yield_module_main_screen_promo_banner_message">Получай до %1$s APY на свой баланс</string>
<string name="yield_module_main_view_approve_notification_description">Разрешение для вашего токена в режиме доходности было отозвано. Откройте токен, чтобы выдать разрешение снова.</string>

View file

@ -0,0 +1,8 @@
package com.tangem.domain.yield.supply.models
import java.math.BigDecimal
data class YieldSupplyFee(
val value: BigDecimal,
val isHighFee: Boolean = false,
)

View file

@ -2,6 +2,8 @@ package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWalletId
@ -10,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.yield.supply.YieldSupplyConst.YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT
import com.tangem.domain.yield.supply.fixFee
import com.tangem.domain.yield.supply.models.YieldSupplyFee
import java.math.BigDecimal
import java.math.RoundingMode
@ -25,7 +28,7 @@ class YieldSupplyGetCurrentFeeUseCase(
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<Throwable, BigDecimal> = catch {
): Either<Throwable, YieldSupplyFee> = catch {
val feeWithoutGas = feeRepository.getEthereumFeeWithoutGas(userWalletId, cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate ?: error("Fiat rate is missing")
@ -60,6 +63,23 @@ class YieldSupplyGetCurrentFeeUseCase(
val tokenValue = rateRatio.multiply(nativeGas.amount.value)
tokenValue.stripTrailingZeros()
val isEthereum = cryptoCurrencyStatus.currency
.network.id.rawId.value == Blockchain.Ethereum.id
val isHighFee = if (isEthereum) {
val maxFeePerGas = (feeWithoutGas as? Fee.Ethereum.EIP1559)?.maxFeePerGas ?: 0.toBigInteger()
maxFeePerGas >= HIGH_ETHEREUM_FEE
} else {
false
}
YieldSupplyFee(
value = tokenValue.stripTrailingZeros(),
isHighFee = isHighFee,
)
}
companion object {
private val HIGH_ETHEREUM_FEE = 400_000_000.toBigInteger()
}
}

View file

@ -0,0 +1,384 @@
package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.utils.convertToSdkAmount
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
@OptIn(ExperimentalCoroutinesApi::class)
class YieldSupplyGetCurrentFeeUseCaseTest {
private val feeRepository: FeeRepository = mockk()
private val quotesRepository: QuotesRepository = mockk()
private val currenciesRepository: CurrenciesRepository = mockk()
private lateinit var useCase: YieldSupplyGetCurrentFeeUseCase
private val userWalletId = UserWalletId("abcdef012345")
@BeforeEach
fun setUp() {
useCase = YieldSupplyGetCurrentFeeUseCase(
feeRepository = feeRepository,
quotesRepository = quotesRepository,
currenciesRepository = currenciesRepository,
)
}
@Test
fun `GIVEN valid inputs on non-ethereum WHEN invoke THEN returns fee value and not high`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val tokenDecimals = 8
val nativeDecimals = 18
val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = nativeDecimals)
val nativeFiatRate = BigDecimal("4.00")
val maxFeePerGas = BigInteger.valueOf(1_000_000_000L)
val feeWithoutGas = Fee.Ethereum.EIP1559(
maxFeePerGas = maxFeePerGas,
priorityFee = BigInteger.ONE,
gasLimit = BigInteger.ZERO,
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus),
)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns feeWithoutGas
coEvery {
currenciesRepository.getNetworkCoin(
userWalletId = userWalletId,
networkId = token.network.id,
derivationPath = token.network.derivationPath,
)
} returns nativeCoin
coEvery {
quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!))
} returns setOf(
QuoteStatus(
rawCurrencyId = nativeCoin.id.rawCurrencyId!!,
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
priceChange = BigDecimal.ZERO,
),
),
)
val gasLimit = BigInteger.valueOf(350_000)
val nativeGasValue = maxFeePerGas.multiply(gasLimit).toBigDecimal().movePointLeft(nativeDecimals)
val rateRatio = nativeFiatRate.divide(cryptoStatus.value.fiatRate!!, tokenDecimals, RoundingMode.HALF_UP)
val expectedTokenValue = rateRatio.multiply(nativeGasValue).stripTrailingZeros()
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isRight()).isTrue()
val fee = (result as Either.Right).value
assertThat(fee.value).isEqualTo(expectedTokenValue)
assertThat(fee.isHighFee).isFalse()
}
@Test
fun `GIVEN ethereum with high gas WHEN invoke THEN returns high fee flag`() = runTest {
val rawNetworkId = Blockchain.Ethereum.id
val tokenDecimals = 8
val nativeDecimals = 18
val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = nativeDecimals)
val nativeFiatRate = BigDecimal("4.00")
val maxFeePerGas = BigInteger.valueOf(400_000_000L) // threshold
val feeWithoutGas = Fee.Ethereum.EIP1559(
maxFeePerGas = maxFeePerGas,
priorityFee = BigInteger.ONE,
gasLimit = BigInteger.ZERO,
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus),
)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns feeWithoutGas
coEvery {
currenciesRepository.getNetworkCoin(
userWalletId = userWalletId,
networkId = token.network.id,
derivationPath = token.network.derivationPath,
)
} returns nativeCoin
coEvery {
quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!))
} returns setOf(
QuoteStatus(
rawCurrencyId = nativeCoin.id.rawCurrencyId!!,
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
priceChange = BigDecimal.ZERO,
),
),
)
val gasLimit = BigInteger.valueOf(350_000)
val nativeGasValue = maxFeePerGas.multiply(gasLimit).toBigDecimal().movePointLeft(nativeDecimals)
val rateRatio = nativeFiatRate.divide(cryptoStatus.value.fiatRate!!, tokenDecimals, RoundingMode.HALF_UP)
val expectedTokenValue = rateRatio.multiply(nativeGasValue).stripTrailingZeros()
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isRight()).isTrue()
val fee = (result as Either.Right).value
assertThat(fee.value).isEqualTo(expectedTokenValue)
assertThat(fee.isHighFee).isTrue()
}
@Test
fun `GIVEN token fiat rate missing WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = null)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559(
maxFeePerGas = BigInteger.ONE,
priorityFee = BigInteger.ONE,
gasLimit = BigInteger.ZERO,
amount = BigDecimal.ZERO.convertToSdkAmount(
createStatus(token = token, fiatRate = BigDecimal.ONE),
),
)
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isLeft()).isTrue()
val error = (result as Either.Left).value
assertThat(error).isInstanceOf(IllegalStateException::class.java)
assertThat(error.message).isEqualTo("Fiat rate is missing")
}
@Test
fun `GIVEN native quotes unavailable WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559(
maxFeePerGas = BigInteger.valueOf(1_000_000_000L),
priorityFee = BigInteger.ONE,
gasLimit = BigInteger.ZERO,
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus),
)
coEvery {
currenciesRepository.getNetworkCoin(
userWalletId = userWalletId,
networkId = token.network.id,
derivationPath = token.network.derivationPath,
)
} returns nativeCoin
coEvery { quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) } returns null
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isLeft()).isTrue()
val error = (result as Either.Left).value
assertThat(error).isInstanceOf(IllegalStateException::class.java)
assertThat(error.message).isEqualTo("Quotes for native coin are unavailable")
}
@Test
fun `GIVEN empty quotes list WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559(
maxFeePerGas = BigInteger.valueOf(1_000_000_000L),
priorityFee = BigInteger.ONE,
gasLimit = BigInteger.ZERO,
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus),
)
coEvery {
currenciesRepository.getNetworkCoin(
userWalletId = userWalletId,
networkId = token.network.id,
derivationPath = token.network.derivationPath,
)
} returns nativeCoin
coEvery { quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) } returns emptySet()
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isLeft()).isTrue()
val error = (result as Either.Left).value
assertThat(error).isInstanceOf(IllegalStateException::class.java)
assertThat(error.message).isEqualTo("Empty quotes list for native coin")
}
@Test
fun `GIVEN native fiat rate non-positive WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559(
maxFeePerGas = BigInteger.valueOf(1_000_000_000L),
priorityFee = BigInteger.ONE,
gasLimit = BigInteger.ZERO,
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus),
)
coEvery {
currenciesRepository.getNetworkCoin(
userWalletId = userWalletId,
networkId = token.network.id,
derivationPath = token.network.derivationPath,
)
} returns nativeCoin
coEvery {
quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!))
} returns setOf(
QuoteStatus(
rawCurrencyId = nativeCoin.id.rawCurrencyId!!,
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO, // non-positive
priceChange = BigDecimal.ZERO,
),
),
)
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isLeft()).isTrue()
val error = (result as Either.Left).value
assertThat(error).isInstanceOf(IllegalArgumentException::class.java)
assertThat(error.message).isEqualTo("Native fiat rate must be > 0")
}
@Test
fun `GIVEN token fiat rate non-positive WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal.ZERO) // non-positive
coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559(
maxFeePerGas = BigInteger.ONE,
priorityFee = BigInteger.ONE,
gasLimit = BigInteger.ZERO,
amount = BigDecimal.ZERO.convertToSdkAmount(
createStatus(token = token, fiatRate = BigDecimal.ONE),
),
)
val result = useCase(userWalletId, cryptoStatus)
assertThat(result.isLeft()).isTrue()
val error = (result as Either.Left).value
assertThat(error).isInstanceOf(IllegalArgumentException::class.java)
assertThat(error.message).isEqualTo("Fiat rate for token must be > 0")
}
private fun createToken(rawNetworkId: String, decimals: Int): CryptoCurrency.Token {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = rawNetworkId, derivationPath = derivationPath),
backendId = rawNetworkId,
name = rawNetworkId,
currencySymbol = rawNetworkId.take(3).uppercase(),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId),
),
network = network,
name = "TEST_TOKEN",
symbol = "TTK",
decimals = decimals,
iconUrl = null,
isCustom = false,
contractAddress = "0xToken",
)
}
private fun createCoin(rawNetworkId: String, decimals: Int): CryptoCurrency.Coin {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = rawNetworkId, derivationPath = derivationPath),
backendId = rawNetworkId,
name = rawNetworkId,
currencySymbol = rawNetworkId.take(3).uppercase(),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
return CryptoCurrency.Coin(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId),
),
network = network,
name = "TEST_COIN",
symbol = "TCN",
decimals = decimals,
iconUrl = null,
isCustom = false,
)
}
private fun createStatus(token: CryptoCurrency.Token, fiatRate: BigDecimal?): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
fiatRate = fiatRate,
priceChange = BigDecimal.ZERO,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x0000000000000000000000000000000000000000",
type = NetworkAddress.Address.Type.Primary,
),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
}
}

View file

@ -283,7 +283,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
YieldSupplyActiveFeeContentTransformer(
cryptoCurrencyStatus = cryptoStatus,
appCurrency = appCurrency,
feeValue = currentFee,
feeValue = currentFee.value,
maxNetworkFee = maxFee,
analyticsHandler = analyticsHandler,
),

View file

@ -43,7 +43,6 @@ internal class YieldSupplyNotificationsComponent(
Modifier.padding(top = 16.dp)
},
containerColor = TangemTheme.colors.background.action,
iconTint = null,
)
}
}

View file

@ -6,4 +6,5 @@ import java.math.BigDecimal
data class YieldSupplyNotificationData(
val feeValue: BigDecimal?,
val feeError: GetFeeError?,
val shouldShowHighFeeNotification: Boolean = false,
)

View file

@ -74,6 +74,10 @@ internal class YieldSupplyNotificationsModel @Inject constructor(
dustValue = null,
onReload = params.callback::onFeeReload,
)
if (data.shouldShowHighFeeNotification) {
add(NotificationUM.Info.YieldSupplyHighNetworkFee)
}
}
if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) {

View file

@ -190,7 +190,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
updatedTransactionList = updatedTransactionList,
feeValue = feeSum,
maxNetworkFee = maxFee,
estimatedFeeValueInTokenCurrency = estimatedFee,
estimatedFeeValueInTokenCurrency = estimatedFee.value,
minAmount = minAmount,
),
)
@ -198,6 +198,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
data = YieldSupplyNotificationData(
feeValue = feeSum,
feeError = null,
shouldShowHighFeeNotification = estimatedFee.isHighFee,
),
)
},