Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-03 16:13:36 +03:00
commit 1706a25d1a
61 changed files with 717 additions and 347 deletions

@ -1 +1 @@
Subproject commit 5b70b3652d06811e3f1292e41e735e00fe75783f
Subproject commit d52c8382e9e94b2b44ceecd7cb7bd897e091073c

View file

@ -152,8 +152,8 @@ internal object StakingDomainModule {
fun provideGetPendingTransactionsUseCase(
stakingPendingTransactionRepository: StakingPendingTransactionRepository,
stakingErrorResolver: StakingErrorResolver,
): GetPendingTransactionsUseCase {
return GetPendingTransactionsUseCase(
): GetStakingPendingTransactionsUseCase {
return GetStakingPendingTransactionsUseCase(
stakingPendingTransactionRepository = stakingPendingTransactionRepository,
stakingErrorResolver = stakingErrorResolver,
)

View file

@ -124,7 +124,7 @@ data class YieldDTO(
@Json(name = "rewardSchedule")
val rewardSchedule: RewardScheduleDTO,
@Json(name = "cooldownPeriod")
val cooldownPeriod: PeriodDTO,
val cooldownPeriod: PeriodDTO?,
@Json(name = "warmupPeriod")
val warmupPeriod: PeriodDTO,
@Json(name = "rewardClaiming")

View file

@ -17,10 +17,10 @@
},
{
"name": "IS_ETHEREUM_EIP_1559_ENABLED",
"version": "5.16.0"
"version": "5.17.0"
},
{
"name": "MIGRATE_USER_COUNTRY_CODE_ENABLED",
"version": "5.16.0"
"version": "5.17.0"
}
]

View file

@ -128,7 +128,7 @@
<string name="common_no_address">Keine Adresse</string>
<string name="common_now">Jetzt</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Primär Karte</string>
<string name="common_origin_card">Hauptkarte</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Einfügen</string>
<string name="common_privacy_policy">Datenschutzbestimmungen</string>
@ -275,7 +275,7 @@
<string name="express_token_list_empty_search">Keine Token gefunden. Bitte versuche eine andere Anfrage</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaktions-ID kopiert</string>
<string name="exсhange_token_description">Wechsel eine Ihrer Währungen in eine andere</string>
<string name="exсhange_token_description">Tauschen jeden Vermögenswert in Deinem Portfolio gegen diesen Token</string>
<string name="feedback_data_collection_message">Die folgenden Angaben sind freiwillig. Du kannst diese löschen, wenn du sie nicht weitergeben möchtest.</string>
<string name="feedback_preface_rate_negative">Teile uns mit, welche Funktionen du vermisst, und wir werden versuchen, dir zu helfen.</string>
<string name="feedback_preface_scan_failed">Bitte sag uns, welche Karte du hast</string>
@ -345,6 +345,7 @@
<string name="manage_tokens_wallet_support_only_one_network_title">Das Wallet unterstützt nicht mehr als ein Netzwerk</string>
<string name="markets_add_to_my_portfolio_description">Um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen, füge diesen Token zu mindestens 1 Netzwerk hinzu</string>
<string name="markets_add_to_my_portfolio_unavailable_description">Dieses Asset ist nicht verfügbar</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Dieses Asset ist für dieses Wallet nicht verfügbar</string>
<string name="markets_add_to_portfolio_button">Zum Portfolio hinzufügen</string>
<string name="markets_add_token">Hinzufügen</string>
<string name="markets_available_networks">Verfügbare Netzwerke</string>
@ -561,7 +562,7 @@
<item quantity="one">%d Wallet</item>
<item quantity="other">%d Wallets</item>
</plurals>
<string name="reset_card_to_factory_button_title">Zurücksetzen der Karte</string>
<string name="reset_card_to_factory_button_title">Karte zurücksetzen</string>
<string name="reset_card_to_factory_condition_1">Mir ist bewusst, dass ich nach der Durchführung dieser Aktion keinen Zugriff mehr auf die aktuelle Wallet habe.</string>
<string name="reset_card_to_factory_condition_2">Mir ist klar, dass ich diese Karte nicht verwenden kann, um meinen Zugangscode auf den anderen Karten der aktuellen Wallet wiederherzustellen</string>
<string name="reset_card_with_backup_to_factory_message">Durch das Zurücksetzen auf Werkseinstellungen wird die Wallet vollständig von der ausgewählten Karte gelöscht. Du kannst die aktuelle Wallet nicht wiederherstellen oder die Karte verwenden, um den Zugangscode wiederherzustellen.</string>
@ -686,7 +687,7 @@
<string name="staking_details_unbonding_period_info">Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden.</string>
<string name="staking_details_warmup_period">Aufwärmphase</string>
<string name="staking_details_warmup_period_info">Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking.</string>
<string name="staking_legal">Indem Du die Staking-Funktionalität nutzen, stimmst Du den %1$s und %2$s des Anbieters zu.</string>
<string name="staking_legal">Indem Du die Staking-Funktionalität nutzt, stimmst Du den %1$s und %2$s des Anbieters zu.</string>
<string name="staking_locked">Gesperrt</string>
<string name="staking_migrate">Migrieren</string>
<string name="staking_native">Natives Staking</string>
@ -718,14 +719,15 @@
<string name="staking_revote">Neuwahl</string>
<string name="staking_reward_claiming_auto">Automatisch</string>
<string name="staking_reward_claiming_manual">Händisch</string>
<string name="staking_reward_schedule_block">Block</string>
<string name="staking_reward_schedule_day">Tag</string>
<string name="staking_reward_schedule_block">Pro Block</string>
<string name="staking_reward_schedule_day">Täglich</string>
<string name="staking_reward_schedule_each_day">Täglich</string>
<string name="staking_reward_schedule_epoch">Epoche</string>
<string name="staking_reward_schedule_era">Ära</string>
<string name="staking_reward_schedule_hour">Stunde</string>
<string name="staking_reward_schedule_month">Monat</string>
<string name="staking_reward_schedule_week">Woche</string>
<string name="staking_reward_schedule_each_sec">Alle %1$s Sek.</string>
<string name="staking_reward_schedule_epoch">Pro Epoche</string>
<string name="staking_reward_schedule_era">Pro Ära</string>
<string name="staking_reward_schedule_hour">Stündlich</string>
<string name="staking_reward_schedule_month">Monatlich</string>
<string name="staking_reward_schedule_week">Wöchentlich</string>
<string name="staking_rewards">Belohnungen</string>
<string name="staking_stake_locked">Stake gesperrt</string>
<string name="staking_stake_more">Mehr staken</string>
@ -743,6 +745,7 @@
<string name="staking_unstaking_item_name">Unstake Assets %s</string>
<string name="staking_validator">Validator/ Prüfer</string>
<string name="staking_validators">Validatoren</string>
<string name="staking_validators_label">Strategischer Partner</string>
<string name="staking_vote">Abstimmung</string>
<string name="staking_vote_locked">Abstimmung gesperrt</string>
<string name="staking_withdraw">Zurückziehen</string>

View file

@ -338,7 +338,7 @@
</plurals>
<string name="manage_tokens_remove">Eliminar</string>
<string name="manage_tokens_search_placeholder">por ejemplo, BTC I trust, hodl I must</string>
<string name="manage_tokens_toast_portfolio_updated">Su cartera ha sido actualizada</string>
<string name="manage_tokens_toast_portfolio_updated">Su portafolio ha sido actualizada</string>
<string name="manage_tokens_unavailable_description">El token seleccionado no está disponible actualmente para acciones dentro de la billetera cripto. Pero no se preocupa, puede expresar su interés votando a favor.</string>
<string name="manage_tokens_unavailable_vote">Votar a favor</string>
<string name="manage_tokens_wallet_selector_title">Elegir wallet</string>
@ -373,7 +373,7 @@
<string name="markets_sort_by_experienced_buyers_title">Compradores experimentados</string>
<string name="markets_sort_by_rating_title">Evaluación</string>
<string name="markets_sort_by_title">Ordenar por</string>
<string name="markets_sort_by_top_gainers_title">Mejores Ganadores</string>
<string name="markets_sort_by_top_gainers_title">Top Ganadores</string>
<string name="markets_sort_by_top_losers_title">Top Perdedores</string>
<string name="markets_sort_by_trending_title">Tendencias</string>
<string name="markets_token_details_about_token_title">Acerca de %s</string>
@ -720,6 +720,7 @@
<string name="staking_reward_schedule_block">Bloquear</string>
<string name="staking_reward_schedule_day">Día</string>
<string name="staking_reward_schedule_each_day">Cada día</string>
<string name="staking_reward_schedule_each_sec">Cada %1$s segundo(s)</string>
<string name="staking_reward_schedule_epoch">Época</string>
<string name="staking_reward_schedule_era">Era</string>
<string name="staking_reward_schedule_hour">Hora</string>
@ -742,8 +743,9 @@
<string name="staking_unstaking_item_name">Detenando el staking de activos %s</string>
<string name="staking_validator">Validador</string>
<string name="staking_validators">Validadores</string>
<string name="staking_validators_label">Socio estratégico</string>
<string name="staking_vote">Votar</string>
<string name="staking_vote_locked">Voto bloqueado</string>
<string name="staking_vote_locked">Votar</string>
<string name="staking_withdraw">Retirar</string>
<string name="staking_your_stakes">Sus stakes</string>
<string name="story_awe_description">Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta</string>

View file

@ -720,6 +720,7 @@
<string name="staking_reward_schedule_block">Bloquer</string>
<string name="staking_reward_schedule_day">Jour</string>
<string name="staking_reward_schedule_each_day">Chaque jour</string>
<string name="staking_reward_schedule_each_sec">Toute(s) les %1$s seconde(s)</string>
<string name="staking_reward_schedule_epoch">Époque</string>
<string name="staking_reward_schedule_era">Ère</string>
<string name="staking_reward_schedule_hour">Heure</string>
@ -742,8 +743,9 @@
<string name="staking_unstaking_item_name">En train de destaker les actifs %s</string>
<string name="staking_validator">Validateur</string>
<string name="staking_validators">Validateurs</string>
<string name="staking_validators_label">Partenaire stratégique</string>
<string name="staking_vote">Voter</string>
<string name="staking_vote_locked">Vote bloqué</string>
<string name="staking_vote_locked">Voter</string>
<string name="staking_withdraw">Retirer</string>
<string name="staking_your_stakes">Vos stakes</string>
<string name="story_awe_description">Stockez vos actifs crypto en toute sécurité tout en conservant les clés privées contenues dans votre carte</string>

View file

@ -95,6 +95,9 @@
<plurals name="common_days">
<item quantity="other">%d 日</item>
</plurals>
<plurals name="common_days_no_param">
<item quantity="other"></item>
</plurals>
<string name="common_delete">削除</string>
<string name="common_disabled">無効</string>
<string name="common_done">完了</string>
@ -340,8 +343,9 @@
<string name="manage_tokens_unavailable_vote">賛成票を投じる</string>
<string name="manage_tokens_wallet_selector_title">ウォレットを選択</string>
<string name="manage_tokens_wallet_support_only_one_network_title">ウォレットは複数のネットワークをサポートしていません。</string>
<string name="markets_add_to_my_portfolio_description">このアセットの購入、交換、受け取りを開始するには、ポートフォリオに追加してください。</string>
<string name="markets_add_to_my_portfolio_description">このアセットを購入・交換・受け取るには、ポートフォリオに追加してください。</string>
<string name="markets_add_to_my_portfolio_unavailable_description">このアセットは利用できません</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">このアセットはこのウォレットでは使用できません。</string>
<string name="markets_add_to_portfolio_button">ポートフォリオに追加</string>
<string name="markets_add_token">追加</string>
<string name="markets_available_networks">利用可能なネットワーク</string>
@ -388,6 +392,11 @@
<string name="markets_token_details_circulating_supply_description">取引可能で市場に流通しているコインの総数</string>
<string name="markets_token_details_circulating_supply_full">循環供給量</string>
<string name="markets_token_details_empty_exchanges">取引所が見つかりません</string>
<string name="markets_token_details_exchange">取引所</string>
<string name="markets_token_details_exchange_trust_score_caution">注意</string>
<string name="markets_token_details_exchange_trust_score_risky">危険</string>
<string name="markets_token_details_exchange_trust_score_trusted">信頼できる</string>
<string name="markets_token_details_exchanges_title">取引所</string>
<string name="markets_token_details_experienced_buyers">経験豊富な買い手</string>
<string name="markets_token_details_experienced_buyers_description">少なくとも100の発信取引を持つネット・バイヤー</string>
<string name="markets_token_details_experienced_buyers_full">経験豊富な買い手</string>
@ -428,6 +437,7 @@
<string name="markets_token_details_trading_volume">取引量24時間</string>
<string name="markets_token_details_trading_volume_24h_description">過去24時間以内に取引された暗号資産の合計額。市場の活発さと流動性を示します。</string>
<string name="markets_token_details_trading_volume_full">取引量24時間</string>
<string name="markets_token_details_volume">取引量</string>
<string name="markets_tooltip_message">これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します</string>
<string name="markets_tooltip_title">トークンを追加</string>
<string name="nfc_error_unavailable">お使いのデバイスではNFCが使用できません</string>
@ -716,7 +726,10 @@
<string name="staking_reward_claiming_manual">手動</string>
<string name="staking_reward_schedule_block">ブロック毎</string>
<string name="staking_reward_schedule_day">毎日</string>
<string name="staking_reward_schedule_each">%sごと</string>
<string name="staking_reward_schedule_each_day">毎日</string>
<string name="staking_reward_schedule_each_days">%1$s日ごとに</string>
<string name="staking_reward_schedule_each_sec">%1$s秒毎</string>
<string name="staking_reward_schedule_epoch">エポック毎</string>
<string name="staking_reward_schedule_era">時代毎</string>
<string name="staking_reward_schedule_hour">毎時間</string>
@ -727,6 +740,7 @@
<string name="staking_stake_more">もっとステーキングする</string>
<string name="staking_summary_description_text">%1$sをステーキングし、年間 %2$sを受け取ります</string>
<string name="staking_tap_to_unlock">タップしてロック解除</string>
<string name="staking_tap_to_unlock_or_vote">タップしてロック解除もしくは投票</string>
<string name="staking_tap_to_withdraw">タップして引き出す</string>
<string name="staking_title_stake">%sをステーキングする</string>
<string name="staking_title_unstake">%sのステーキング解除</string>
@ -739,8 +753,9 @@
<string name="staking_unstaking_item_name">ステーキング解除中の資産%s</string>
<string name="staking_validator">バリデーター</string>
<string name="staking_validators">バリデーター</string>
<string name="staking_validators_label">戦略的パートナー</string>
<string name="staking_vote">投票する</string>
<string name="staking_vote_locked">投票はロックされていま</string>
<string name="staking_vote_locked">投票す</string>
<string name="staking_withdraw">引き出す</string>
<string name="staking_your_stakes">あなたのステーキング</string>
<string name="story_awe_description">カード内に秘密鍵を保管しながら暗号資産を安全に保管します</string>
@ -804,6 +819,7 @@
<string name="transaction_history_operation">オペレーション</string>
<string name="transaction_history_transaction_from_address">送金元: %s</string>
<string name="transaction_history_transaction_to_address">送金先: %s</string>
<string name="transaction_history_transaction_validator">バリデーター: %s</string>
<string name="try_to_load_data_again_button_title">もう一度やり直してください</string>
<string name="twin_error_same_card">同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。</string>
<string name="twin_error_wrong_twin">間違ったツインカードをスキャンしました。別のカードをお試しください。</string>

View file

@ -101,6 +101,12 @@
<item quantity="many">%d дней</item>
<item quantity="other">%d дней</item>
</plurals>
<plurals name="common_days_no_param">
<item quantity="one">день</item>
<item quantity="few">дня</item>
<item quantity="many">дней</item>
<item quantity="other">дней</item>
</plurals>
<string name="common_delete">Удалить</string>
<string name="common_disabled">Отключено</string>
<string name="common_done">Готово</string>
@ -403,6 +409,9 @@
<string name="markets_token_details_circulating_supply_full">Циркулирующее предложение</string>
<string name="markets_token_details_empty_exchanges">Биржи не найдены</string>
<string name="markets_token_details_exchange">Биржа</string>
<string name="markets_token_details_exchange_trust_score_caution">Осторожно</string>
<string name="markets_token_details_exchange_trust_score_risky">Рискованно</string>
<string name="markets_token_details_exchange_trust_score_trusted">Надежно</string>
<string name="markets_token_details_exchanges_title">Биржи</string>
<string name="markets_token_details_experienced_buyers">Опытные трейдеры</string>
<string name="markets_token_details_experienced_buyers_description">Покупатели, у которых было как минимум 100 исходящих транзакций.</string>
@ -445,7 +454,7 @@
<string name="markets_token_details_trading_volume_24h_description">Общая сумма криптовалюты, которая была продана за последние 24 часа, показывающая уровень активности и ликвидности на рынке.</string>
<string name="markets_token_details_trading_volume_full">Объем торгов (24ч)</string>
<string name="markets_token_details_volume">Объем</string>
<string name="markets_tooltip_message">Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из маркета</string>
<string name="markets_tooltip_message">Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка</string>
<string name="markets_tooltip_title">Добавить токены</string>
<string name="nfc_error_unavailable">Функция NFC недоступна на вашем устройстве</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
@ -520,7 +529,7 @@
<string name="onboarding_subtitle_scan_primary_card_format">Подготовьте основную карту с номером %s</string>
<string name="onboarding_subtitle_success_tangem_wallet_onboarding">Ваша карта настроена и готова к использованию.</string>
<string name="onboarding_subtitle_two_backup_cards">Добавлено максимальное количество карт. Завершите процесс резервного копирования.</string>
<string name="onboarding_title">Активация карты</string>
<string name="onboarding_title">Активация кошелька</string>
<string name="onboarding_title_backup_card_format">Резервная карта #%d</string>
<string name="onboarding_title_no_backup_cards">Нет резервных карт</string>
<string name="onboarding_title_notifications">Уведомления</string>
@ -745,6 +754,8 @@
<string name="staking_reward_schedule_block">За блок</string>
<string name="staking_reward_schedule_day">Ежедневно</string>
<string name="staking_reward_schedule_each_day">Каждый день</string>
<string name="staking_reward_schedule_each_days">Каждые %1$s дня</string>
<string name="staking_reward_schedule_each_sec">Каждые %1$s сек</string>
<string name="staking_reward_schedule_epoch">За эпоху</string>
<string name="staking_reward_schedule_era">За эру</string>
<string name="staking_reward_schedule_hour">Ежечасно</string>
@ -755,6 +766,7 @@
<string name="staking_stake_more">Застейкать еще</string>
<string name="staking_summary_description_text">Вы стейкаете %1$s и будете получать %2$s ежегодно</string>
<string name="staking_tap_to_unlock">Нажмите для разблокировки</string>
<string name="staking_tap_to_unlock_or_vote">Нажмите для разблокировки или голосования</string>
<string name="staking_tap_to_withdraw">Нажмите для вывода</string>
<string name="staking_title_stake">Застейкать %s</string>
<string name="staking_title_unstake">Вывести %s</string>
@ -767,8 +779,9 @@
<string name="staking_unstaking_item_name">Завершение стейкинга %s</string>
<string name="staking_validator">Валидатор</string>
<string name="staking_validators">Валидаторы</string>
<string name="staking_validators_label">Стратегический партнер</string>
<string name="staking_vote">Проголосовать</string>
<string name="staking_vote_locked">Голосование заблокировано</string>
<string name="staking_vote_locked">Проголосовать</string>
<string name="staking_withdraw">Вывод</string>
<string name="staking_your_stakes">Застейканные средства</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>

View file

@ -141,7 +141,7 @@
<string name="common_read_more">Детальніше</string>
<string name="common_receive">Отримати</string>
<string name="common_reject">Відхилити</string>
<string name="common_reload">Перезавантажити</string>
<string name="common_reload">Оновити</string>
<string name="common_rename">Перейменувати</string>
<string name="common_save">Зберегти</string>
<string name="common_save_changes">Зберегти зміни</string>
@ -351,6 +351,7 @@
<string name="manage_tokens_wallet_support_only_one_network_title">Гаманець не підтримує більше однієї мережі</string>
<string name="markets_add_to_my_portfolio_description">Щоб почати купувати, обмінювати або отримувати цей актив, додайте цей токен принаймні в 1 мережу</string>
<string name="markets_add_to_my_portfolio_unavailable_description">Цей актив недоступний</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Цей токен недоступний для цього гаманця</string>
<string name="markets_add_to_portfolio_button">Додати в портфоліо</string>
<string name="markets_add_token">Додати</string>
<string name="markets_available_networks">Доступні мережі</string>
@ -401,6 +402,7 @@
<string name="markets_token_details_experienced_buyers_full">Досвідчені покупці</string>
<string name="markets_token_details_fully_diluted_valuation">Повн. розведена кап.</string>
<string name="markets_token_details_fully_diluted_valuation_description">Загальна теоретична вартість криптовалюти, якщо всі монети, які могли б існувати, перебувають в обігу, включаючи ті, що не перебувають в обігу в даний час</string>
<string name="markets_token_details_fully_diluted_valuation_full">Повністю розведена капіталізація</string>
<string name="markets_token_details_genesis_date">Дата створення</string>
<string name="markets_token_details_high">Високий</string>
<string name="markets_token_details_holders">Тримачі</string>
@ -415,9 +417,11 @@
<string name="markets_token_details_low">Низький</string>
<string name="markets_token_details_market_capitalization">Рин. кап.</string>
<string name="markets_token_details_market_capitalization_description">Загальна ринкова вартість криптовалюти, що розраховується шляхом множення поточної ціни монети на загальну кількість монет в обігу</string>
<string name="markets_token_details_market_capitalization_full">Ринкова капіталізація</string>
<string name="markets_token_details_market_rating">Рейтинг</string>
<string name="markets_token_details_market_rating_description">Позиція в крипторейтингу між усіма монетами на основі ринкової капіталізації</string>
<string name="markets_token_details_market_rating_full">Рейтинг</string>
<string name="markets_token_details_max_supply">Макс. обсяг</string>
<string name="markets_token_details_max_supply_description">Максимальна кількість монет або токенів, яка може коли-небудь існувати для певної криптовалюти</string>
<string name="markets_token_details_max_supply_full">Максимальна пропозиція</string>
<string name="markets_token_details_metrics">Метрики</string>
@ -713,6 +717,7 @@
<string name="staking_notification_earn_rewards_text_period_week">Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щотиждня.</string>
<string name="staking_notification_earn_rewards_text_weekly">Стейкайте безпечно та почніть отримувати винагороди щотижня</string>
<string name="staking_notification_earn_rewards_title">Отримуйте винагороду за стейкінг</string>
<string name="staking_notification_network_error_text">Стейкінг тимчасово недоступний через проблеми в мережі. Будь ласка, спробуйте пізніше.</string>
<string name="staking_notification_new_validator_funds_transfer">Стейкінг в мережі %1$s з новим валідатором автоматично переведе всі раніше застейкані кошти до цього валідатора</string>
<string name="staking_notification_restake_rewards_text">Реінвестуйте зароблені винагороди у суму стейкінгу, щоб збільшити потенційний прибуток.</string>
<string name="staking_notification_unlock_text">Розблокуйте свої кошти, щоб вивести їх зі стейкінгу. Розблокування займе %s.</string>
@ -732,6 +737,7 @@
<string name="staking_reward_schedule_block">Блок</string>
<string name="staking_reward_schedule_day">День</string>
<string name="staking_reward_schedule_each_day">Щодня</string>
<string name="staking_reward_schedule_each_sec">Кожні %1$s сек</string>
<string name="staking_reward_schedule_epoch">Епоха</string>
<string name="staking_reward_schedule_era">Ера</string>
<string name="staking_reward_schedule_hour">Година</string>
@ -754,6 +760,7 @@
<string name="staking_unstaking_item_name">Виведення зі стейкінгу %s</string>
<string name="staking_validator">Валідатор</string>
<string name="staking_validators">Валідатори</string>
<string name="staking_validators_label">Стратегічний партнер</string>
<string name="staking_vote">Проголосувати</string>
<string name="staking_vote_locked">Голос заблоковано</string>
<string name="staking_withdraw">Зняти</string>
@ -813,12 +820,13 @@
<string name="token_swap_promotion_button">Обміняти</string>
<string name="transaction_history_contract_address">контракт: %s</string>
<string name="transaction_history_empty_transactions">У вас ще немає транзакцій</string>
<string name="transaction_history_error_failed_to_load">Не вдалося завантажити історію транзакцій.\nНатисніть кнопку перезавантаження, щоб оновити інформацію.</string>
<string name="transaction_history_error_failed_to_load">Не вдалося завантажити історію транзакцій.\nНатисніть кнопку нижче, щоб оновити інформацію</string>
<string name="transaction_history_multiple_addresses">Кілька адрес</string>
<string name="transaction_history_not_supported_description">Історія транзакцій наразі не підтримується для цього блокчейну. Але не хвилюйтеся, ми працюємо над цим! А поки можете перевірити це в оглядачі.</string>
<string name="transaction_history_operation">Операція</string>
<string name="transaction_history_transaction_from_address">від: %s</string>
<string name="transaction_history_transaction_to_address">до: %s</string>
<string name="transaction_history_transaction_validator">валідатор: %s</string>
<string name="try_to_load_data_again_button_title">Спробуйте знову</string>
<string name="twin_error_same_card">Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d</string>
<string name="twin_error_wrong_twin">Ви відсканували не ту twin-картку. Будь ласка, спробуйте відсканувати іншу</string>

View file

@ -97,6 +97,10 @@
<item quantity="one">%d day</item>
<item quantity="other">%d days</item>
</plurals>
<plurals name="common_days_no_param">
<item quantity="one">day</item>
<item quantity="other">days</item>
</plurals>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_done">Done</string>
@ -128,7 +132,7 @@
<string name="common_no_address">No address</string>
<string name="common_now">Now</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Primary Card</string>
<string name="common_origin_card">Primary card</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Paste</string>
<string name="common_privacy_policy">Privacy Policy</string>
@ -511,7 +515,7 @@
<string name="onboarding_subtitle_scan_primary_card_format">Prepare the primary card with number %s</string>
<string name="onboarding_subtitle_success_tangem_wallet_onboarding">Your wallet card is configured and ready for use.</string>
<string name="onboarding_subtitle_two_backup_cards">Max number of cards added. Finalize the backup process.</string>
<string name="onboarding_title">Activating card</string>
<string name="onboarding_title">Activating wallet</string>
<string name="onboarding_title_backup_card_format">Backup card #%d</string>
<string name="onboarding_title_no_backup_cards">No backup cards</string>
<string name="onboarding_title_notifications">Notifications</string>
@ -573,7 +577,7 @@
<item quantity="one">%d wallet</item>
<item quantity="other">%d wallets</item>
</plurals>
<string name="reset_card_to_factory_button_title">Reset the Card</string>
<string name="reset_card_to_factory_button_title">Reset the card</string>
<string name="reset_card_to_factory_condition_1">I understand that after performing this action, I will no longer have access to the current wallet</string>
<string name="reset_card_to_factory_condition_2">I realize that I can\'t use this card to recover my access code on the other cards of the current wallet</string>
<string name="reset_card_with_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.</string>
@ -589,7 +593,7 @@
<string name="save_user_wallet_agreement_enroll_biometrics_title">Enable biometric authorization</string>
<string name="save_user_wallet_agreement_header_biometrics">Would you like to use biometrics?</string>
<string name="save_user_wallet_agreement_notice">Note that making a transaction with your funds will still require your card</string>
<string name="scan_card_settings_button">Scan Card</string>
<string name="scan_card_settings_button">Scan card</string>
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
@ -691,7 +695,7 @@
<string name="staking_details_reward_claiming">Reward claiming</string>
<string name="staking_details_reward_claiming_info">Method of receiving staking rewards.\nIt can be either automatic, where the reward is credited to your address, or manual, where you need to withdraw the reward by creating a transaction to receive it.</string>
<string name="staking_details_reward_schedule">Reward schedule</string>
<string name="staking_details_reward_schedule_info">This is a schedule that determines when participants in staking receive their rewards.</string>
<string name="staking_details_reward_schedule_info">This is a schedule that determines when participants in staking receive their rewards. The reward distribution time may vary slightly depending on the validator and network load.</string>
<string name="staking_details_rewards_to_claim">Rewards: %s</string>
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Unbonding Period</string>
@ -732,7 +736,10 @@
<string name="staking_reward_claiming_manual">Manual</string>
<string name="staking_reward_schedule_block">Per Block</string>
<string name="staking_reward_schedule_day">Daily</string>
<string name="staking_reward_schedule_each_day">Daily</string>
<string name="staking_reward_schedule_each">Each %s</string>
<string name="staking_reward_schedule_each_day">Each day</string>
<string name="staking_reward_schedule_each_days">Each %1$s days</string>
<string name="staking_reward_schedule_each_sec">Each %1$s sec</string>
<string name="staking_reward_schedule_epoch">Per Epoch</string>
<string name="staking_reward_schedule_era">Per Era</string>
<string name="staking_reward_schedule_hour">Hourly</string>
@ -743,6 +750,7 @@
<string name="staking_stake_more">Stake more</string>
<string name="staking_summary_description_text">You stake %1$s and will be receiving %2$s yearly</string>
<string name="staking_tap_to_unlock">Tap to unlock</string>
<string name="staking_tap_to_unlock_or_vote">Tap to unlock or vote</string>
<string name="staking_tap_to_withdraw">Tap to withdraw</string>
<string name="staking_title_stake">Stake %s</string>
<string name="staking_title_unstake">Unstake %s</string>
@ -755,8 +763,9 @@
<string name="staking_unstaking_item_name">Unstaking assets %s</string>
<string name="staking_validator">Validator</string>
<string name="staking_validators">Validators</string>
<string name="staking_validators_label">Strategic partner</string>
<string name="staking_vote">Vote</string>
<string name="staking_vote_locked">Vote locked</string>
<string name="staking_vote_locked">Vote</string>
<string name="staking_withdraw">Withdraw</string>
<string name="staking_your_stakes">Your stakes</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
@ -843,7 +852,7 @@
<string name="user_wallet_list_error_wallet_already_saved">This wallet has already been saved, you can add another one</string>
<string name="user_wallet_list_rename_popup_error_already_exists">The wallet with name %s already exists</string>
<string name="user_wallet_list_rename_popup_placeholder">Wallet name</string>
<string name="user_wallet_list_rename_popup_title">Rename Wallet</string>
<string name="user_wallet_list_rename_popup_title">Rename wallet</string>
<string name="user_wallet_list_unlock_all">Unlock all</string>
<string name="user_wallet_list_unlock_all_with">Unlock all with %s</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>

View file

@ -24,7 +24,6 @@ import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -33,15 +32,17 @@ import com.tangem.core.ui.res.TangemThemePreview
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2841-1589&t=u6pOF6lsdpvWLELb-4)
*
* @param subtitle subtitle text
* @param caption caption text
* @param modifier modifier
* @param caption caption text
* @param imageUrl icon to load
* @param iconRes icon resource
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param iconTint icon tint
* @param isGrayscaleImage whether to display grayscale image
* @param subtitleEndIconRes icon to show after subtitle
* @param iconEndRes icon to end of row
* @param onImageError composable to show if image loading failed
* @param subtitleExtraContent subtitle extra content
* @param extraContent extra content
*/
@Suppress("LongMethod")
@ -56,10 +57,9 @@ internal fun InputRowImageBase(
captionColor: Color = TangemTheme.colors.text.tertiary,
iconTint: Color = TangemTheme.colors.icon.informative,
isGrayscaleImage: Boolean = false,
@DrawableRes subtitleEndIconRes: Int? = null,
subtitleEndIconTint: Color = TangemColorPalette.Azure,
@DrawableRes iconEndRes: Int? = null,
onImageError: (@Composable () -> Unit)? = null,
subtitleExtraContent: (@Composable RowScope.() -> Unit)? = null,
extraContent: (@Composable RowScope.() -> Unit)? = null,
) {
Row(
@ -100,10 +100,7 @@ internal fun InputRowImageBase(
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
SubtitleEndIconRes(
subtitleEndIconRes = subtitleEndIconRes,
subtitleEndIconTint = subtitleEndIconTint,
)
subtitleExtraContent?.invoke(this)
}
if (caption != null) {
Text(
@ -124,23 +121,6 @@ internal fun InputRowImageBase(
}
}
@Composable
private fun RowScope.SubtitleEndIconRes(subtitleEndIconRes: Int?, subtitleEndIconTint: Color) {
AnimatedVisibility(
visible = subtitleEndIconRes != null,
label = "Subtitle end icon visibility animation",
modifier = Modifier.align(Alignment.CenterVertically),
) {
val icon = remember(this) { requireNotNull(subtitleEndIconRes) }
Icon(
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = icon)),
tint = subtitleEndIconTint,
contentDescription = null,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
}
@Composable
private fun RowScope.InputRowEndIcon(iconRes: Int?) {
AnimatedVisibility(

View file

@ -2,14 +2,21 @@ package com.tangem.core.ui.components.inputrow
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -17,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -24,17 +32,19 @@ import com.tangem.core.ui.res.TangemThemePreview
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2841-1589&t=u6pOF6lsdpvWLELb-4)
*
* @param title title text
* @param subtitle subtitle text
* @param caption caption text
* @param infoTitle info title text
* @param infoSubtitle info subtitle text
* @param modifier modifier
* @param title title text
* @param caption caption text
* @param infoSubtitle info subtitle text
* @param imageUrl icon to load
* @param iconRes icon resource
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param iconTint icon color
* @param isGrayscaleImage whether to display grayscale image
* @param showPendingIcon whether to show pending icon
* @param subtitleEndIconRes subtitle icon to end of row
* @param iconEndRes icon to end of row
* @param onImageError composable to show if image loading failed
*/
@ -78,9 +88,14 @@ fun InputRowImageInfo(
subtitleColor = subtitleColor,
captionColor = captionColor,
isGrayscaleImage = isGrayscaleImage,
subtitleEndIconRes = subtitleEndIconRes,
iconEndRes = iconEndRes,
onImageError = onImageError,
subtitleExtraContent = {
SubtitleEndIconRes(
subtitleEndIconRes = subtitleEndIconRes,
subtitleEndIconTint = TangemColorPalette.Azure,
)
},
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
@ -120,6 +135,23 @@ fun InputRowImageInfo(
}
}
@Composable
private fun RowScope.SubtitleEndIconRes(subtitleEndIconRes: Int?, subtitleEndIconTint: Color) {
AnimatedVisibility(
visible = subtitleEndIconRes != null,
label = "Subtitle end icon visibility animation",
modifier = Modifier.align(Alignment.CenterVertically),
) {
val icon = remember(this) { requireNotNull(subtitleEndIconRes) }
Icon(
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = icon)),
tint = subtitleEndIconTint,
contentDescription = null,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
@ -40,8 +41,9 @@ import java.math.BigDecimal
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param isSelected true if selected
* @param selectorContent selector content
* @param onImageError composable to show if image loading failed
* @param subtitleExtraContent subtitle selector content
* @param selectorContent selector content
*/
@Composable
fun InputRowImageSelector(
@ -54,6 +56,7 @@ fun InputRowImageSelector(
captionColor: Color = TangemTheme.colors.text.tertiary,
isSelected: Boolean = false,
onImageError: (@Composable () -> Unit)? = null,
subtitleExtraContent: (@Composable RowScope.() -> Unit)? = null,
selectorContent: @Composable ((isSelected: Boolean, isEnabled: Boolean, onSelect: () -> Unit) -> Unit),
) {
InputRowImageBase(
@ -63,6 +66,7 @@ fun InputRowImageSelector(
subtitleColor = subtitleColor,
captionColor = captionColor,
onImageError = onImageError,
subtitleExtraContent = subtitleExtraContent,
modifier = modifier
.clickable(
onClick = onSelect,

View file

@ -75,6 +75,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"sui", "sui/test" -> R.drawable.img_sui_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
else -> R.drawable.ic_alert_24
@ -153,6 +154,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"sui", "sui/test" -> R.drawable.img_sui_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
else -> R.drawable.ic_alert_24
@ -228,6 +230,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
"internet-computer" -> R.drawable.img_icp_22
"sui", "sui/test" -> R.drawable.img_sui_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
else -> R.drawable.ic_alert_24
@ -306,6 +309,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
"internet-computer" -> R.drawable.ic_icp_22
"sui", "sui/test" -> R.drawable.ic_sui_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22
else -> R.drawable.ic_alert_24
@ -384,6 +388,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
"internet-computer" -> R.drawable.ic_icp_22
"sui", "sui/test" -> R.drawable.ic_sui_22
"energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22
"energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_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:pathData="M14.8,9.7C15.4,10.5 15.8,11.5 15.8,12.7C15.8,13.8 15.4,14.9 14.7,15.7L14.7,15.8L14.7,15.7C14.6,15.6 14.6,15.5 14.6,15.4C14.3,14 13.2,12.7 11.4,11.7C10.2,11 9.5,10.1 9.3,9.2C9.2,8.6 9.3,8 9.4,7.5C9.6,6.9 9.9,6.5 10.1,6.2L10.8,5.4C10.9,5.2 11.1,5.2 11.2,5.4L14.8,9.7H14.8ZM15.9,8.8L11.2,3.1C11.1,3 10.9,3 10.8,3.1L6.1,8.8L6.1,8.8C5.3,9.9 4.8,11.3 4.8,12.8C4.8,16.2 7.6,19 11,19C14.5,19 17.3,16.2 17.3,12.8C17.3,11.3 16.7,9.9 15.9,8.8L15.9,8.8L15.9,8.8ZM7.3,9.7L7.7,9.1L7.7,9.2C7.7,9.3 7.7,9.4 7.7,9.5C8,10.9 9,12.1 10.6,13C12,13.8 12.8,14.7 13.1,15.7C13.2,16.2 13.2,16.6 13.1,16.9L13.1,17L13.1,17C12.5,17.3 11.8,17.5 11,17.5C8.4,17.5 6.2,15.3 6.2,12.7C6.2,11.5 6.6,10.5 7.3,9.7L7.3,9.7Z"
android:fillColor="#000" />
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<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="M0,0h22v22h-22z"
android:fillColor="#4CA3FF"/>
<path
android:pathData="M14.8,9.7C15.4,10.5 15.8,11.5 15.8,12.7C15.8,13.8 15.4,14.9 14.7,15.7L14.7,15.8L14.7,15.7C14.6,15.6 14.6,15.5 14.6,15.4C14.3,14 13.2,12.7 11.4,11.7C10.2,11 9.5,10.1 9.3,9.2C9.2,8.6 9.3,8 9.4,7.5C9.6,6.9 9.9,6.5 10.1,6.2L10.8,5.4C10.9,5.2 11.1,5.2 11.2,5.4L14.8,9.7H14.8ZM15.9,8.8L11.2,3.1C11.1,3 10.9,3 10.8,3.1L6.1,8.8L6.1,8.8C5.3,9.9 4.8,11.3 4.8,12.8C4.8,16.2 7.6,19 11,19C14.5,19 17.3,16.2 17.3,12.8C17.3,11.3 16.7,9.9 15.9,8.8L15.9,8.8L15.9,8.8ZM7.3,9.7L7.7,9.1L7.7,9.2C7.7,9.3 7.7,9.4 7.7,9.5C8,10.9 9,12.1 10.6,13C12,13.8 12.8,14.7 13.1,15.7C13.2,16.2 13.2,16.6 13.1,16.9L13.1,17L13.1,17C12.5,17.3 11.8,17.5 11,17.5C8.4,17.5 6.2,15.3 6.2,12.7C6.2,11.5 6.6,10.5 7.3,9.7L7.3,9.7Z"
android:fillColor="#ffffff" />
</vector>

View file

@ -51,8 +51,8 @@ internal class DefaultCustomTokensRepository(
Blockchain.Binance,
Blockchain.BinanceTestnet,
-> true
Blockchain.Cardano -> blockchain.validateContractAddress(contractAddress.lowercase())
else -> blockchain.validateAddress(contractAddress.lowercase())
Blockchain.Cardano -> blockchain.validateContractAddress(contractAddress)
else -> blockchain.validateAddress(contractAddress)
}
}

View file

@ -12,10 +12,12 @@ import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.supportedBlockchains
@ -30,6 +32,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher.Request
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -39,6 +42,7 @@ internal class DefaultManageTokensRepository(
private val userWalletsStore: UserWalletsStore,
private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
private val appPreferencesStore: AppPreferencesStore,
private val testnetTokensStorage: TestnetTokensStorage,
private val dispatchers: CoroutineDispatcherProvider,
) : ManageTokensRepository {
@ -58,7 +62,6 @@ internal class DefaultManageTokensRepository(
).toBatchFlow()
}
@Suppress("ComplexCondition")
private fun createFetcher(
batchSize: Int,
): LimitOffsetBatchFetcher<ManageTokensListConfig, List<ManagedCryptoCurrency>> = LimitOffsetBatchFetcher(
@ -66,59 +69,101 @@ internal class DefaultManageTokensRepository(
batchSize = batchSize,
subFetcher = { request, _, isFirstBatchFetching ->
val userWallet = request.params.userWalletId?.let { getUserWallet(it) }
val supportedBlockchains = getSupportedBlockchains(userWallet)
val searchText = request.params.searchText?.takeIf { it.isNotBlank() }
val call = suspend {
tangemTechApi.getCoins(
networkIds = supportedBlockchains.joinToString(
separator = ",",
transform = Blockchain::toNetworkId,
),
active = true,
searchText = searchText,
offset = request.offset * request.limit,
limit = request.limit,
).getOrThrow()
}
val coinsResponse = if (isFirstBatchFetching) {
call()
if (userWallet?.scanResponse?.card?.isTestCard == true) {
fetchTestnetCurrencies(userWallet, request)
} else {
retryOnError(call = call)
fetchCurrencies(userWallet, request, isFirstBatchFetching)
}
// filter l2 coins
val updatedCoinsResponse = coinsResponse.copy(
coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) },
)
val tokensResponse = request.params.userWalletId?.let { getSavedUserTokensResponseSync(it) }
val items = if (isFirstBatchFetching &&
tokensResponse != null &&
userWallet != null &&
request.params.searchText.isNullOrBlank()
) {
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
scanResponse = userWallet.scanResponse,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
scanResponse = userWallet?.scanResponse,
)
}
BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = items.size < request.limit,
)
},
)
@Suppress("ComplexCondition")
private suspend fun fetchCurrencies(
userWallet: UserWallet?,
request: Request<ManageTokensListConfig>,
isFirstBatchFetching: Boolean,
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
val supportedBlockchains = getSupportedBlockchains(userWallet)
val call = suspend {
tangemTechApi.getCoins(
networkIds = supportedBlockchains.joinToString(
separator = ",",
transform = Blockchain::toNetworkId,
),
active = true,
searchText = request.params.searchText,
offset = request.offset * request.limit,
limit = request.limit,
).getOrThrow()
}
val coinsResponse = if (isFirstBatchFetching) {
call()
} else {
retryOnError(call = call)
}
// filter l2 coins
val updatedCoinsResponse = coinsResponse.copy(
coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) },
)
val tokensResponse = request.params.userWalletId?.let { getSavedUserTokensResponseSync(it) }
val items = if (isFirstBatchFetching &&
tokensResponse != null &&
userWallet != null &&
request.params.searchText.isNullOrBlank()
) {
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
scanResponse = userWallet.scanResponse,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
scanResponse = userWallet?.scanResponse,
)
}
return BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = items.size < request.limit,
)
}
private suspend fun fetchTestnetCurrencies(
userWallet: UserWallet,
request: Request<ManageTokensListConfig>,
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
val searchText = request.params.searchText
val testnetTokensConfig = testnetTokensStorage.getConfig()
val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens(
testnetTokensConfig = if (!searchText.isNullOrBlank()) {
testnetTokensConfig.copy(
tokens = testnetTokensConfig.tokens.filter { token ->
token.symbol.contains(other = searchText, ignoreCase = true) ||
token.name.contains(other = searchText, ignoreCase = true)
},
)
} else {
testnetTokensConfig
},
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
scanResponse = userWallet.scanResponse,
)
return BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = true,
)
}
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"

View file

@ -4,6 +4,7 @@ import com.tangem.data.managetokens.DefaultCustomTokensRepository
import com.tangem.data.managetokens.DefaultManageTokensRepository
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.managetokens.repository.CustomTokensRepository
@ -27,6 +28,7 @@ internal object ManageTokensDataModule {
userWalletsStore: UserWalletsStore,
manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
appPreferencesStore: AppPreferencesStore,
testnetTokensStorage: TestnetTokensStorage,
dispatchers: CoroutineDispatcherProvider,
): ManageTokensRepository {
return DefaultManageTokensRepository(
@ -34,6 +36,7 @@ internal object ManageTokensDataModule {
userWalletsStore,
manageTokensUpdateFetcher,
appPreferencesStore,
testnetTokensStorage,
dispatchers,
)
}

View file

@ -12,6 +12,7 @@ import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.currency.getTokenId
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.util.cardTypesResolver
@ -38,20 +39,49 @@ internal class ManagedCryptoCurrencyFactory {
tokensResponse: UserTokensResponse,
scanResponse: ScanResponse,
): List<ManagedCryptoCurrency> {
val customTokens = tokensResponse.tokens
.mapNotNull { token ->
maybeCreateCustomToken(token, coinsResponse.imageHost, scanResponse)
}
val tokens = create(
coinsResponse,
tokensResponse,
scanResponse,
)
val customTokens = createCustomTokens(tokensResponse, scanResponse)
val tokens = create(coinsResponse, tokensResponse, scanResponse)
return customTokens + tokens
}
fun createTestnetWithCustomTokens(
testnetTokensConfig: TestnetTokensConfig,
tokensResponse: UserTokensResponse?,
scanResponse: ScanResponse,
): List<ManagedCryptoCurrency> {
val customTokens = tokensResponse
?.let { createCustomTokens(it, scanResponse) }
?: emptyList()
val testnetTokens = testnetTokensConfig.tokens.map { testnetToken ->
ManagedCryptoCurrency.Token(
id = ManagedCryptoCurrency.ID(testnetToken.id),
name = testnetToken.name,
symbol = testnetToken.symbol,
iconUrl = getIconUrl(testnetToken.id),
availableNetworks = testnetToken.networks?.mapNotNull { network ->
createSource(
networkId = network.id,
contractAddress = network.address,
decimals = network.decimalCount ?: return@mapNotNull null,
scanResponse = scanResponse,
)
} ?: emptyList(),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, scanResponse),
)
}
return customTokens + testnetTokens
}
private fun createCustomTokens(
tokensResponse: UserTokensResponse,
scanResponse: ScanResponse,
): List<ManagedCryptoCurrency> = tokensResponse.tokens
.mapNotNull { token ->
maybeCreateCustomToken(token, null, scanResponse)
}
private fun maybeCreateCustomToken(
token: UserTokensResponse.Token,
imageHost: String?,
@ -108,18 +138,25 @@ internal class ManagedCryptoCurrencyFactory {
symbol = coinResponse.symbol,
iconUrl = getIconUrl(coinResponse.id, imageHost),
availableNetworks = updatedNetworks.mapNotNull { network ->
createSource(network, scanResponse)
createSource(
networkId = network.networkId,
contractAddress = network.contractAddress,
decimals = network.decimalCount?.toInt() ?: return@mapNotNull null,
scanResponse = scanResponse,
)
},
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, scanResponse),
)
}
private fun createSource(
networkResponse: CoinsResponse.Coin.Network,
networkId: String,
contractAddress: String?,
decimals: Int,
scanResponse: ScanResponse?,
extraDerivationPath: String? = null,
): SourceNetwork? {
val blockchain = Blockchain.fromNetworkId(networkResponse.networkId)
val blockchain = Blockchain.fromNetworkId(networkId)
?.takeIf { it.isSupportedInApp() }
?: return null
@ -131,7 +168,6 @@ internal class ManagedCryptoCurrencyFactory {
it.card.canHandleToken(blockchain, it.cardTypesResolver)
} ?: true,
) ?: return null
val contractAddress = networkResponse.contractAddress
return if (contractAddress.isNullOrBlank()) {
SourceNetwork.Main(
@ -142,7 +178,7 @@ internal class ManagedCryptoCurrencyFactory {
} else {
SourceNetwork.Default(
network = network,
decimals = requireNotNull(networkResponse.decimalCount?.toInt()),
decimals = decimals,
contractAddress = contractAddress,
)
}
@ -175,7 +211,7 @@ internal class ManagedCryptoCurrencyFactory {
}
}
private fun getIconUrl(id: String, imageHost: String?): String {
private fun getIconUrl(id: String, imageHost: String? = DEFAULT_IMAGE_HOST): String {
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
}

View file

@ -120,7 +120,7 @@ internal class DefaultMarketsTokenRepository(
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart {
) = withContext(dispatcherProvider.io) {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinChart(
currency = fiatCurrencyCode,
@ -137,7 +137,7 @@ internal class DefaultMarketsTokenRepository(
response.getOrThrow()
}
return TokenChartConverter.convert(
return@withContext TokenChartConverter.convert(
interval = interval,
value = result,
@ -157,7 +157,7 @@ internal class DefaultMarketsTokenRepository(
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart {
) = withContext(dispatcherProvider.io) {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinsListCharts(
coinIds = mappedTokenId,
@ -171,7 +171,7 @@ internal class DefaultMarketsTokenRepository(
)
}
return TokenChartConverter.convert(
return@withContext TokenChartConverter.convert(
interval = interval,
value = chart,
@ -191,7 +191,7 @@ internal class DefaultMarketsTokenRepository(
tokenId: String,
tokenSymbol: String,
languageCode: String,
): TokenMarketInfo {
) = withContext(dispatcherProvider.io) {
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
@ -208,29 +208,30 @@ internal class DefaultMarketsTokenRepository(
}
val resultResponse = result.applyL2Compatibility(tokenId)
return TokenMarketInfoConverter.convert(resultResponse)
return@withContext TokenMarketInfoConverter.convert(resultResponse)
}
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String): TokenQuotes {
// for second markets iteration we should use extended api method with all required fields
val response = tangemTechApi.getQuotes(
currencyId = fiatCurrencyCode,
coinIds = tokenId,
fields = marketsQuoteFields.joinToString(separator = ","),
)
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String) =
withContext(dispatcherProvider.io) {
// for second markets iteration we should use extended api method with all required fields
val response = tangemTechApi.getQuotes(
currencyId = fiatCurrencyCode,
coinIds = tokenId,
fields = marketsQuoteFields.joinToString(separator = ","),
)
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
),
) {
response.getOrThrow()
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
),
) {
response.getOrThrow()
}
return@withContext TokenQuotesShortConverter.convert(tokenId, result).toFull()
}
return TokenQuotesShortConverter.convert(tokenId, result).toFull()
}
override suspend fun createCryptoCurrency(
userWalletId: UserWalletId,
token: TokenMarketParams,

View file

@ -3,25 +3,30 @@ package com.tangem.data.staking
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
internal class DefaultStakingPendingTransactionRepository : StakingPendingTransactionRepository {
private val pendingTransactions = CopyOnWriteArrayList<PendingTransaction>()
private val pendingTransactionsMap = ConcurrentHashMap<UserWalletId, CopyOnWriteArrayList<PendingTransaction>>()
override fun saveTransaction(transaction: PendingTransaction) {
pendingTransactions.add(transaction)
override fun saveTransaction(userWalletId: UserWalletId, transaction: PendingTransaction) {
val transactions = pendingTransactionsMap.computeIfAbsent(userWalletId) { CopyOnWriteArrayList() }
transactions.add(transaction)
}
override fun removeTransactions(transactions: Set<PendingTransaction>) {
pendingTransactions.removeAll(transactions)
override fun removeTransactions(userWalletId: UserWalletId, transactions: Set<PendingTransaction>) {
pendingTransactionsMap[userWalletId]?.removeAll(transactions)
}
override fun getTransactionsWithBalanceItems(): List<Pair<PendingTransaction, BalanceItem>> {
return pendingTransactions.mapNotNull { pendingTransaction ->
override fun getTransactionsWithBalanceItems(
userWalletId: UserWalletId,
): List<Pair<PendingTransaction, BalanceItem>> {
return pendingTransactionsMap[userWalletId]?.mapNotNull { pendingTransaction: PendingTransaction ->
PendingTransactionItemConverter.convert(pendingTransaction)?.let { balanceItem ->
pendingTransaction to balanceItem
}
}
} ?: emptyList()
}
}

View file

@ -98,8 +98,7 @@ internal class DefaultStakingRepository(
)
private val yieldBalanceConverter = YieldBalanceConverter()
private val yieldBalanceListConverter = YieldBalanceListConverter()
private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter)
private val isYieldBalanceFetching = MutableStateFlow(
value = emptyMap<UserWalletId, Boolean>(),
@ -121,7 +120,7 @@ internal class DefaultStakingRepository(
key = YIELDS_STORE_KEY,
skipCache = refresh,
block = {
val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = true)
val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)
.getOrThrow()
stakingYieldsStore.store(stakingTokensWithYields.data)

View file

@ -10,8 +10,8 @@ internal object PendingTransactionItemConverter : Converter<PendingTransaction,
override fun convert(value: PendingTransaction): BalanceItem? {
return BalanceItem(
groupId = value.groupId ?: return null,
type = value.type ?: return null,
amount = value.amount ?: return null,
type = value.type,
amount = value.amount,
rawCurrencyId = value.rawCurrencyId,
validatorAddress = value.validator?.address,
date = DateTime.now(),

View file

@ -3,7 +3,6 @@ package com.tangem.data.staking.converters
import com.tangem.data.staking.converters.action.PendingActionConverter
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.utils.converter.Converter
internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBalance> {
@ -27,9 +26,7 @@ internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBa
validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0),
date = item.date?.toDateTime(),
pendingActions = pendingActionConverter
.convertList(item.pendingActions)
// temporarily exclude VOTE_LOCKED, it will be implemented in future iterations
.filterNot { it.type == StakingActionType.VOTE_LOCKED },
.convertList(item.pendingActions),
isPending = false,
)
},

View file

@ -4,18 +4,16 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.utils.converter.Converter
internal class YieldBalanceListConverter : Converter<Set<YieldBalanceWrapperDTO>, YieldBalanceList> {
internal val converter by lazy(LazyThreadSafetyMode.NONE) {
YieldBalanceConverter()
}
internal class YieldBalanceListConverter(
private val yieldBalanceConverter: YieldBalanceConverter,
) : Converter<Set<YieldBalanceWrapperDTO>, YieldBalanceList> {
override fun convert(value: Set<YieldBalanceWrapperDTO>): YieldBalanceList {
return if (value.isEmpty()) {
YieldBalanceList.Empty
} else {
YieldBalanceList.Data(
balances = value.map(converter::convert),
balances = value.map(yieldBalanceConverter::convert),
)
}
}

View file

@ -28,7 +28,7 @@ class YieldConverter(
metadata = convertMetadata(value.metadata),
validators = value.validators
.asSequence()
.filter { it.preferred && it.status == ValidatorStatusDTO.ACTIVE }
.filter { it.status == ValidatorStatusDTO.ACTIVE }
.sortedByDescending { it.apr }
.map { convertValidator(it) }
.toImmutableList(),
@ -88,7 +88,7 @@ class YieldConverter(
tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) },
type = metadataDTO.type,
rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule),
cooldownPeriod = convertPeriod(metadataDTO.cooldownPeriod),
cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) },
warmupPeriod = convertPeriod(metadataDTO.warmupPeriod),
rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming),
defaultValidator = metadataDTO.defaultValidator,
@ -123,6 +123,7 @@ class YieldConverter(
stakedBalance = validatorDTO.stakedBalance,
votingPower = validatorDTO.votingPower,
preferred = validatorDTO.preferred,
isStrategicPartner = isStrategicPartner(validatorDTO.address),
)
}
@ -174,4 +175,12 @@ class YieldConverter(
else -> Yield.Args.ArgType.UNKNOWN
}
}
private fun isStrategicPartner(validatorAddress: String): Boolean = PARTNERS.any { it == validatorAddress }
private companion object {
val PARTNERS = listOf(
"cosmosvaloper1wrx0x9m9ykdhw9sg04v7uljme53wuj03aa5d4f",
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras
import com.tangem.blockchain.blockchains.icp.ICPTransactionExtras
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
@ -249,6 +250,7 @@ internal class DefaultTransactionRepository(
)
}
@Suppress("CyclomaticComplexMethod")
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
val blockchain = Blockchain.fromId(networkId)
if (memo == null) return null
@ -263,12 +265,14 @@ internal class DefaultTransactionRepository(
Blockchain.Binance -> BinanceTransactionExtras(memo)
Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) }
Blockchain.Cosmos,
Blockchain.Sei,
Blockchain.TerraV1,
Blockchain.TerraV2,
-> CosmosTransactionExtras(memo)
Blockchain.TON -> TonTransactionExtras(memo)
Blockchain.Hedera -> HederaTransactionExtras(memo)
Blockchain.Algorand -> AlgorandTransactionExtras(memo)
Blockchain.InternetComputer -> memo.toLongOrNull()?.let { ICPTransactionExtras(it) }
else -> null
}
}

View file

@ -11,9 +11,10 @@ android {
dependencies {
api(projects.domain.staking.models)
api(projects.domain.core)
implementation(deps.kotlin.serialization)
implementation(deps.jodatime)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)

View file

@ -6,8 +6,9 @@ import java.math.BigDecimal
data class PendingTransaction(
val groupId: String?,
val type: BalanceType?,
val amount: BigDecimal?,
val type: BalanceType,
val amount: BigDecimal,
val rawCurrencyId: String?,
val validator: Yield.Validator?,
val balancesId: Int,
)

View file

@ -1,14 +1,7 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.Yield
import java.math.BigDecimal
data class SubmitHashData(
val transactionHash: String,
val transactionId: String,
val validator: Yield.Validator?,
val amount: BigDecimal?,
val balanceType: BalanceType?,
val rawCurrencyId: String?,
val pendingTransaction: PendingTransaction,
)

View file

@ -62,6 +62,7 @@ data class Yield(
val stakedBalance: String? = null,
val votingPower: Double? = null,
val preferred: Boolean,
val isStrategicPartner: Boolean,
) {
enum class ValidatorStatus {
@ -84,7 +85,7 @@ data class Yield(
val tokens: List<Token>,
val type: String,
val rewardSchedule: RewardSchedule,
val cooldownPeriod: Period,
val cooldownPeriod: Period?,
val warmupPeriod: Period,
val rewardClaiming: RewardClaiming,
val defaultValidator: String?,

View file

@ -32,6 +32,14 @@ sealed class YieldBalance {
.distinctBy { it.validatorAddress }
.size
}
fun getBalancesUniqueId(): Int {
// need to exclude rewards because their amount may change frequently
return balance.items
.filter { it.type != BalanceType.REWARDS }
.map { it.amount.toString() + it.type.toString() + it.groupId }
.hashCode()
}
}
data object Empty : YieldBalance()

View file

@ -5,18 +5,19 @@ import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for getting saved staking pending transactions.
*/
class GetPendingTransactionsUseCase(
class GetStakingPendingTransactionsUseCase(
private val stakingPendingTransactionRepository: StakingPendingTransactionRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(): Either<StakingError, List<BalanceItem>> {
operator fun invoke(userWalletId: UserWalletId): Either<StakingError, List<BalanceItem>> {
return Either.catch {
stakingPendingTransactionRepository.getTransactionsWithBalanceItems().map { it.second }
stakingPendingTransactionRepository.getTransactionsWithBalanceItems(userWalletId).map { it.second }
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -3,52 +3,74 @@ package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
import java.util.UUID
class InvalidatePendingTransactionsUseCase(
private val stakingPendingTransactionRepository: StakingPendingTransactionRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(yieldBalance: YieldBalance): Either<StakingError, List<BalanceItem>> {
operator fun invoke(
userWalletId: UserWalletId,
balanceItems: List<BalanceItem>,
balancesId: Int,
): Either<StakingError, List<BalanceItem>> {
return Either.catch {
if (yieldBalance is YieldBalance.Data) {
val (balancesToDisplay, transactionsToRemove) = mergeRealAndPendingTransactions(
real = yieldBalance.balance.items,
pending = stakingPendingTransactionRepository.getTransactionsWithBalanceItems(),
)
val (balancesToDisplay, transactionsToRemove) = mergeRealAndPendingTransactions(
realData = balanceItems,
newBalancesId = balancesId,
pendingData = stakingPendingTransactionRepository.getTransactionsWithBalanceItems(userWalletId),
)
stakingPendingTransactionRepository.removeTransactions(transactionsToRemove.toSet())
stakingPendingTransactionRepository.removeTransactions(userWalletId, transactionsToRemove.toSet())
balancesToDisplay
} else {
emptyList()
}
balancesToDisplay
}.mapLeft {
stakingErrorResolver.resolve(it)
}
}
private fun mergeRealAndPendingTransactions(
real: List<BalanceItem>,
pending: List<Pair<PendingTransaction, BalanceItem>>,
realData: List<BalanceItem>,
newBalancesId: Int,
pendingData: List<Pair<PendingTransaction, BalanceItem>>,
): Pair<List<BalanceItem>, List<PendingTransaction>> {
val map = real.associateBy { Triple(it.groupId, it.type, it.amount) }.toMutableMap()
val balances = realData.associateBy { Triple(it.groupId, it.type, it.amount) }.toMutableMap()
val toRemove = mutableListOf<PendingTransaction>()
val transactionsToRemove = mutableListOf<PendingTransaction>()
pending.forEach { (pendingTransaction, balanceItem) ->
pendingData.forEach { (pendingTransaction, balanceItem) ->
val key = Triple(balanceItem.groupId, balanceItem.type, balanceItem.amount)
if (map.containsKey(key)) {
map[key] = balanceItem
} else {
toRemove.add(pendingTransaction)
val oldBalancesId = pendingTransaction.balancesId
when {
newBalancesId != oldBalancesId -> {
transactionsToRemove.add(pendingTransaction)
}
balances.containsKey(key) -> {
balances[key] = balanceItem
}
else -> {
val groupId = UUID.randomUUID().toString()
balances[Triple(groupId, BalanceType.STAKED, pendingTransaction.amount)] = BalanceItem(
groupId = groupId,
type = pendingTransaction.type,
amount = pendingTransaction.amount,
rawCurrencyId = pendingTransaction.rawCurrencyId,
validatorAddress = pendingTransaction.validator?.address,
date = null,
pendingActions = emptyList(),
isPending = true,
)
}
}
}
return map.values.toList() to toRemove
return balances.values.toList() to transactionsToRemove
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for saving hash that failed to submit during staking confirmation
@ -14,9 +15,12 @@ class SavePendingTransactionUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(pendingTransaction: PendingTransaction): Either<StakingError, Unit> {
operator fun invoke(userWalletId: UserWalletId, transaction: PendingTransaction): Either<StakingError, Unit> {
return Either.catch {
stakingPendingTransactionRepository.saveTransaction(pendingTransaction)
stakingPendingTransactionRepository.saveTransaction(
userWalletId = userWalletId,
transaction = transaction,
)
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -2,12 +2,13 @@ package com.tangem.domain.staking.repositories
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.wallets.models.UserWalletId
interface StakingPendingTransactionRepository {
fun getTransactionsWithBalanceItems(): List<Pair<PendingTransaction, BalanceItem>>
fun getTransactionsWithBalanceItems(userWalletId: UserWalletId): List<Pair<PendingTransaction, BalanceItem>>
fun saveTransaction(transaction: PendingTransaction)
fun saveTransaction(userWalletId: UserWalletId, transaction: PendingTransaction)
fun removeTransactions(transactions: Set<PendingTransaction>)
fun removeTransactions(userWalletId: UserWalletId, transactions: Set<PendingTransaction>)
}

View file

@ -36,6 +36,8 @@ internal class SendRecipientMemoFieldConverter(
Blockchain.Stellar.id,
Blockchain.Hedera.id,
Blockchain.Algorand.id,
Blockchain.Sei.id,
Blockchain.InternetComputer.id,
-> {
convert(
value = Data(memo = memo, label = R.string.send_extras_hint_memo),

View file

@ -85,17 +85,12 @@ internal object StakingNotification {
),
)
data class PendingAction(
data class Ordinary(
val title: TextReference,
val text: TextReference,
) : StakingNotification.Info(
title = title,
subtitle = text,
)
data object TronRevote : StakingNotification.Info(
title = resourceReference(R.string.staking_revote),
subtitle = resourceReference(R.string.staking_notifications_revote_tron_text),
)
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
@ -98,7 +99,7 @@ internal sealed class StakingStates {
val transactionDoneState: TransactionDoneState,
val isApprovalNeeded: Boolean,
val reduceAmountBy: BigDecimal?,
val balanceState: BalanceState?,
val possiblePendingTransaction: PendingTransaction?,
) : ConfirmationState()
data class Empty(

View file

@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -83,7 +84,11 @@ internal class BalanceItemConverter(
private fun getSubtitle(balance: BalanceItem) = when (balance.type) {
BalanceType.UNSTAKING -> getUnbondingDate(balance.date)
BalanceType.UNSTAKED -> resourceReference(R.string.staking_tap_to_withdraw)
BalanceType.LOCKED -> resourceReference(R.string.staking_tap_to_unlock)
BalanceType.LOCKED -> if (balance.pendingActions.any { it.type == StakingActionType.VOTE_LOCKED }) {
resourceReference(R.string.staking_tap_to_unlock_or_vote)
} else {
resourceReference(R.string.staking_tap_to_unlock)
}
BalanceType.PREPARING -> {
val warmupPeriod = yield.metadata.warmupPeriod.days
combinedReference(
@ -100,8 +105,8 @@ internal class BalanceItemConverter(
-> null
}
private fun getUnbondingDate(date: DateTime?): TextReference {
val unbondingPeriod = yield.metadata.cooldownPeriod.days
private fun getUnbondingDate(date: DateTime?): TextReference? {
val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null
if (date == null) {
return combinedReference(
resourceReference(R.string.staking_details_unbonding_period),

View file

@ -28,12 +28,17 @@ internal class YieldBalancesConverter(
val cryptoCurrency = cryptoCurrencyStatus.currency
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
val balanceToShowItems = balancesToShowProvider()
return if (yieldBalance is YieldBalance.Data) {
val cryptoRewardsValue = yieldBalance.getRewardStakingBalance()
val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue)
return if (yieldBalance is YieldBalance.Data || balanceToShowItems.any { it.isPending }) {
val cryptoRewardsValue = (yieldBalance as? YieldBalance.Data)?.getRewardStakingBalance()
val balanceToShowItems = balancesToShowProvider()
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val fiatRewardsValue = if (fiatRate != null && cryptoRewardsValue != null) {
fiatRate.times(cryptoRewardsValue)
} else {
null
}
InnerYieldBalanceState.Data(
rewardsCrypto = BigDecimalFormatter.formatCryptoAmount(

View file

@ -8,10 +8,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
@ -35,6 +32,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import java.math.BigDecimal
import java.util.UUID
@Suppress("LongParameterList")
internal class StakingTransactionSender @AssistedInject constructor(
@ -91,12 +89,16 @@ internal class StakingTransactionSender @AssistedInject constructor(
sendStakingTransaction(
fullTransactionsData = fullTransactionsData,
balanceState = confirmationState.balanceState,
possiblePendingTransaction = composePendingTransaction(confirmationState),
onSendSuccess = onSendSuccess,
onSendError = onSendError,
)
}
private fun composePendingTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction {
return confirmationState.possiblePendingTransaction ?: composeStakeTransaction(confirmationState)
}
private suspend fun getStakingTransactions(
state: StakingUiState,
confirmationState: StakingStates.ConfirmationState.Data,
@ -204,7 +206,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
private suspend fun sendStakingTransaction(
fullTransactionsData: List<FullTransactionData>,
balanceState: BalanceState?,
possiblePendingTransaction: PendingTransaction,
onSendSuccess: (txUrl: String) -> Unit,
onSendError: (SendTransactionError?) -> Unit,
) {
@ -218,13 +220,9 @@ internal class StakingTransactionSender @AssistedInject constructor(
},
ifRight = { transactionHashes ->
submitHash(
transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id },
transactions = fullTransactionsData.map { it.stakeKitTransaction },
transactionHashes = transactionHashes,
groupId = balanceState?.groupId,
validator = balanceState?.validator,
amount = balanceState?.cryptoDecimal,
balanceType = balanceState?.type,
rawCurrencyId = balanceState?.rawCurrencyId,
pendingTransaction = possiblePendingTransaction,
)
val txUrl = getExplorerTransactionUrlUseCase(
txHash = transactionHashes.last(),
@ -238,25 +236,18 @@ internal class StakingTransactionSender @AssistedInject constructor(
}
private suspend fun submitHash(
transactionIds: List<String>,
transactions: List<StakingTransaction>,
transactionHashes: List<String>,
groupId: String?,
validator: Yield.Validator?,
amount: BigDecimal?,
balanceType: BalanceType?,
rawCurrencyId: String?,
pendingTransaction: PendingTransaction,
) {
transactionIds
transactions
.zip(transactionHashes)
.forEach { (transactionId, transactionHash) ->
.forEach { (transaction, transactionHash) ->
submitHashUseCase(
SubmitHashData(
transactionId = transactionId,
transactionId = transaction.id,
transactionHash = transactionHash,
validator = validator,
amount = amount,
balanceType = balanceType,
rawCurrencyId = rawCurrencyId,
pendingTransaction = pendingTransaction,
),
)
.onLeft { error ->
@ -267,20 +258,14 @@ internal class StakingTransactionSender @AssistedInject constructor(
),
)
saveUnsubmittedHashUseCase.invoke(
transactionId = transactionId,
transactionId = transaction.id,
transactionHash = transactionHash,
)
}.onRight {
Timber.d("Successful hash submission")
savePendingTransactionUseCase.invoke(
PendingTransaction(
groupId = groupId,
type = balanceType,
amount = amount,
rawCurrencyId = rawCurrencyId,
validator = validator,
),
)
if (transaction.type != StakingTransactionType.FREEZE_ENERGY) {
savePendingTransactionUseCase.invoke(userWallet.walletId, pendingTransaction)
}
}
}
}
@ -299,6 +284,19 @@ internal class StakingTransactionSender @AssistedInject constructor(
)
}
private fun composeStakeTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction {
val state = stateController.value
return PendingTransaction(
groupId = UUID.randomUUID().toString(),
type = BalanceType.STAKED,
amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO,
rawCurrencyId = cryptoCurrencyStatus.currency.id.rawCurrencyId,
validator = (confirmationState.validatorState as? ValidatorState.Content)?.chosenValidator,
balancesId = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.getBalancesUniqueId() ?: 0,
)
}
private data class FullTransactionData(
val stakeKitTransaction: StakingTransaction,
val tangemTransaction: TransactionData.Compiled,

View file

@ -28,6 +28,7 @@ internal object ConfirmationStatePreviewData {
website = "https://luganodes.com/",
votingPower = 0.09778360195377911,
preferred = true,
isStrategicPartner = false,
),
Yield.Validator(
address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32",
@ -40,6 +41,7 @@ internal object ConfirmationStatePreviewData {
website = "https://infstones.com/",
votingPower = 0.0034366257754399774,
preferred = true,
isStrategicPartner = false,
),
Yield.Validator(
address = "0xd14a87025109013b0a2354a775cb335f926af65a",
@ -52,6 +54,7 @@ internal object ConfirmationStatePreviewData {
website = "https://infstones.com/",
votingPower = 0.023487238579718264,
preferred = true,
isStrategicPartner = true,
),
)
@ -93,6 +96,6 @@ internal object ConfirmationStatePreviewData {
isApprovalNeeded = false,
reduceAmountBy = null,
pendingActions = null,
balanceState = null,
possiblePendingTransaction = null,
)
}

View file

@ -86,6 +86,7 @@ internal object InitialStakingStatePreview {
stakedBalance = null,
votingPower = null,
preferred = false,
isStrategicPartner = false,
),
pendingActions = persistentListOf(),
isClickable = true,

View file

@ -255,30 +255,23 @@ internal class AddStakingNotificationsTransformer(
}
private fun MutableList<NotificationUM>.addExitInfoNotifications() {
add(
StakingNotification.Info.Unstake(
cooldownPeriodDays = yield.metadata.cooldownPeriod.days,
subtitleRes = if (isCosmos(cryptoCurrencyStatusProvider().currency.network.id.value)) {
R.string.staking_notification_unstake_cosmos_text
} else {
R.string.staking_notification_unstake_text
},
),
)
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
if (cooldownPeriodDays != null) {
add(
StakingNotification.Info.Unstake(
cooldownPeriodDays = cooldownPeriodDays,
subtitleRes = if (isCosmos(cryptoCurrencyStatusProvider().currency.network.id.value)) {
R.string.staking_notification_unstake_cosmos_text
} else {
R.string.staking_notification_unstake_text
},
),
)
}
}
private fun MutableList<NotificationUM>.addEnterInfoNotifications() {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value)
val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance
?.items?.any {
it.type == BalanceType.PREPARING ||
it.type == BalanceType.STAKED ||
it.type == BalanceType.LOCKED
} == true
if (hasStakedBalance && isTron) {
add(StakingNotification.Info.TronRevote)
}
addTronRevoteNotification()
}
private fun MutableList<NotificationUM>.addPendingInfoNotifications(prevState: StakingUiState) {
@ -298,28 +291,55 @@ internal class AddStakingNotificationsTransformer(
resourceReference(R.string.staking_notification_withdraw_text)
}
StakingActionType.UNLOCK_LOCKED -> {
val cooldownPeriodDays = yield.metadata.cooldownPeriod.days
resourceReference(R.string.staking_unlocked_locked) to resourceReference(
R.string.staking_notification_unlock_text,
wrappedList(
pluralReference(
id = R.plurals.common_days,
count = cooldownPeriodDays,
formatArgs = wrappedList(cooldownPeriodDays),
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
if (cooldownPeriodDays != null) {
resourceReference(R.string.staking_unlocked_locked) to resourceReference(
R.string.staking_notification_unlock_text,
wrappedList(
pluralReference(
id = R.plurals.common_days,
count = cooldownPeriodDays,
formatArgs = wrappedList(cooldownPeriodDays),
),
),
),
)
)
} else {
null to null
}
}
StakingActionType.VOTE_LOCKED -> {
resourceReference(R.string.staking_revote) to
resourceReference(R.string.staking_notifications_revote_tron_text)
}
else -> null to null
}
if (titleReference != null && textReference != null) {
add(
StakingNotification.Info.PendingAction(
StakingNotification.Info.Ordinary(
title = titleReference,
text = textReference,
),
)
}
}
private fun MutableList<NotificationUM>.addTronRevoteNotification() {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value)
val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance
?.items?.any {
it.type == BalanceType.PREPARING ||
it.type == BalanceType.STAKED ||
it.type == BalanceType.LOCKED
} == true
if (isTron && hasStakedBalance) {
add(
StakingNotification.Info.Ordinary(
title = resourceReference(R.string.staking_revote),
text = resourceReference(R.string.staking_notifications_revote_tron_text),
),
)
}
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.utils.transformer.Transformer
internal class SetBalanceStateTransformer(
private val balanceState: BalanceState? = null,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
?: return prevState
return prevState.copy(
confirmationState = possibleConfirmationState.copy(balanceState = balanceState),
)
}
}

View file

@ -144,9 +144,19 @@ internal class SetButtonsStateTransformer(
clickIntents.onAmountValueChange("") // reset amount state
clickIntents.onNextClick(StakingActionCommonType.ENTER)
}
StakingStep.Validators,
StakingStep.Amount,
-> clickIntents.onNextClick()
StakingStep.Validators -> {
if (confirmationState is StakingStates.ConfirmationState.Data) {
clickIntents.onNextClick(
pendingActions = confirmationState.pendingActions,
pendingAction = confirmationState.pendingAction,
)
} else {
clickIntents.onNextClick()
}
}
StakingStep.Amount -> {
clickIntents.onNextClick()
}
StakingStep.Confirmation -> onConfirmationClick()
StakingStep.RewardsValidators -> Unit
}

View file

@ -20,9 +20,12 @@ internal class SetConfirmationStateLoadingTransformer(
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val filteredValidators = yield.validators.filter {
it.preferred
}
val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val possibleValidatorState = possibleConfirmationState?.validatorState as? ValidatorState.Content
val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0]
val chosenValidator = possibleValidatorState?.chosenValidator ?: filteredValidators[0]
return prevState.copy(
confirmationState = StakingStates.ConfirmationState.Data(
@ -32,7 +35,7 @@ internal class SetConfirmationStateLoadingTransformer(
validatorState = ValidatorState.Content(
isClickable = true,
chosenValidator = chosenValidator,
availableValidators = yield.validators,
availableValidators = filteredValidators,
),
notifications = persistentListOf(),
footerText = getFooter(prevState, chosenValidator),
@ -41,7 +44,7 @@ internal class SetConfirmationStateLoadingTransformer(
pendingActions = possibleConfirmationState?.pendingActions,
isApprovalNeeded = false,
reduceAmountBy = null,
balanceState = null,
possiblePendingTransaction = null,
),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.utils.transformer.Transformer
internal object SetConfirmationStateResetAssentTransformer : Transformer<StakingUiState> {
@ -13,7 +14,7 @@ internal object SetConfirmationStateResetAssentTransformer : Transformer<Staking
confirmationState.copy(
isPrimaryButtonEnabled = true,
innerState = InnerConfirmationStakingState.ASSENT,
validatorState = confirmationState.validatorState.copySealed(isClickable = true),
validatorState = ValidatorState.Loading,
)
} else {
confirmationState

View file

@ -104,7 +104,7 @@ internal class SetInitialDataStateTransformer(
return listOfNotNull(
createAnnualPercentageRateItem(yield.validators),
createAvailableItem(cryptoCurrencyStatus),
createUnbondingPeriodItem(yield.metadata.cooldownPeriod.days),
createUnbondingPeriodItem(yield.metadata.cooldownPeriod?.days),
createMinimumRequirementItem(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum,
@ -140,7 +140,8 @@ internal class SetInitialDataStateTransformer(
)
}
private fun createUnbondingPeriodItem(cooldownPeriodDays: Int): RoundedListWithDividersItemData {
private fun createUnbondingPeriodItem(cooldownPeriodDays: Int?): RoundedListWithDividersItemData? {
cooldownPeriodDays ?: return null
return RoundedListWithDividersItemData(
id = R.string.staking_details_unbonding_period,
startText = TextReference.Res(R.string.staking_details_unbonding_period),
@ -231,7 +232,7 @@ internal class SetInitialDataStateTransformer(
pendingActions = null,
isApprovalNeeded = isApprovalNeeded,
reduceAmountBy = null,
balanceState = null,
possiblePendingTransaction = null,
)
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.utils.transformer.Transformer
internal class SetPossiblePendingTransactionTransformer(
private val balanceState: BalanceState,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val possibleConfirmationState =
prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
val balancesId = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.getBalancesUniqueId() ?: 0
return prevState.copy(
confirmationState = possibleConfirmationState.copy(
possiblePendingTransaction = PendingTransaction(
groupId = balanceState.groupId,
type = balanceState.type,
amount = balanceState.cryptoDecimal,
rawCurrencyId = balanceState.rawCurrencyId,
validator = balanceState.validator,
balancesId = balancesId,
),
),
)
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -39,6 +40,7 @@ internal fun StakingConfirmationContent(
) {
if (state !is StakingStates.ConfirmationState.Data) return
val isEnterAction = type == StakingActionCommonType.ENTER
val showValidatorBlock = isEnterAction || state.pendingAction?.type == StakingActionType.VOTE_LOCKED
val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED
Column(
modifier = Modifier
@ -63,7 +65,7 @@ internal fun StakingConfirmationContent(
isEditingDisabled = !isEnterAction && state.innerState != InnerConfirmationStakingState.COMPLETED,
onClick = clickIntents::onPrevClick,
)
if (isEnterAction) {
if (showValidatorBlock) {
ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators)
}
StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent)

View file

@ -5,16 +5,22 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.inputrow.InputRowImageSelector
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
@ -78,6 +84,9 @@ internal fun StakingValidatorListContent(
false,
)
.background(TangemTheme.colors.background.action),
subtitleExtraContent = {
ValidatorLabel(item.isStrategicPartner)
},
selectorContent = { checked, _, _ ->
AnimatedVisibility(
modifier = Modifier,
@ -99,6 +108,23 @@ internal fun StakingValidatorListContent(
}
}
@Composable
private fun RowScope.ValidatorLabel(isStrategicPartner: Boolean) {
if (isStrategicPartner) {
Text(
text = stringResource(R.string.staking_validators_label),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.constant,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 6.dp)
.clip(RoundedCornerShape(6.dp))
.background(TangemTheme.colors.text.accent)
.padding(horizontal = 8.dp),
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -133,9 +133,12 @@ internal class StakingViewModel @Inject constructor(
private val balancesToShow: List<BalanceItem>
get() {
return cryptoCurrencyStatus.value.yieldBalance?.let {
invalidatePendingTransactionsUseCase(it).getOrElse { emptyList() }
} ?: emptyList()
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
return invalidatePendingTransactionsUseCase(
userWalletId = userWallet.walletId,
balanceItems = yieldBalance?.balance?.items ?: emptyList(),
balancesId = yieldBalance?.getBalancesUniqueId() ?: 0,
).getOrElse { emptyList() }
}
private var isInitialInfoAnalyticSent: Boolean = false
@ -215,13 +218,31 @@ internal class StakingViewModel @Inject constructor(
stakingStateRouter.onNextClick()
when {
isInitState() -> {
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency))
stateController.update(SetBalanceStateTransformer(balanceState))
if (balanceState != null) {
stateController.update(SetPossiblePendingTransactionTransformer(balanceState, cryptoCurrencyStatus))
}
stateController.update(
transformer = SetInitialDataStateTransformer(
clickIntents = this@StakingViewModel,
yield = yield,
isAnyTokenStaked = true,
isApprovalNeeded = stakingApproval is StakingApproval.Needed,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider { appCurrency },
balancesToShowProvider = Provider { balancesToShow },
),
)
onRefreshSwipe(isRefreshing = false)
}
isAssentState() -> {
getFee(pendingAction, pendingActions)
stateController.update(SetBalanceStateTransformer(balanceState))
if (balanceState != null) {
stateController.update(SetPossiblePendingTransactionTransformer(balanceState, cryptoCurrencyStatus))
}
val amountState = value.amountState as? AmountState.Data
if (amountState?.amountTextField?.isWarning == true) {
stateController.update(
@ -325,6 +346,7 @@ internal class StakingViewModel @Inject constructor(
if (value.currentStep == StakingStep.Confirmation) {
when ((value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState) {
InnerConfirmationStakingState.ASSENT -> {
stateController.update(SetConfirmationStateResetAssentTransformer)
stakingStateRouter.onPrevClick()
}
null,

View file

@ -14,6 +14,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
@ -38,6 +39,7 @@ internal class TokenDetailsLoadedBalanceConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingEntryInfoProvider: Provider<StakingEntryInfo?>,
private val pendingBalancesProvider: Provider<List<BalanceItem>>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val symbol: String,
private val decimals: Int,
@ -145,6 +147,8 @@ internal class TokenDetailsLoadedBalanceConverter(
val stakingEntryInfo = stakingEntryInfoProvider.invoke()
val stakingAvailability = stakingAvailabilityProvider.invoke()
val iconState = state.tokenInfoBlockState.iconState
val pendingBalances = pendingBalancesProvider.invoke()
val fiatRate = status.value.fiatRate
return when {
stakingAvailability == StakingAvailability.TemporaryUnavailable -> {
@ -153,14 +157,24 @@ internal class TokenDetailsLoadedBalanceConverter(
stakingAvailability == StakingAvailability.Unavailable -> {
null
}
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> {
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null && pendingBalances.isEmpty() -> {
getStakeAvailableState(stakingEntryInfo, iconState)
}
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null && pendingBalances.isNotEmpty() -> {
val pendingBalancesCryptoAmount = pendingBalances.sumOf { it.amount }
val stakingFiatAmount = fiatRate?.multiply(pendingBalancesCryptoAmount)
getStakedState(
status = status,
stakingCryptoAmount = pendingBalancesCryptoAmount,
stakingFiatAmount = stakingFiatAmount,
stakingRewardAmount = null,
)
}
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> {
null
}
else -> {
val fiatRate = status.value.fiatRate
val stakingRewardAmount = yieldBalance?.getRewardStakingBalance()?.let { fiatRate?.multiply(it) }
val stakingFiatAmount = stakingCryptoAmount?.let { fiatRate?.multiply(it) }

View file

@ -20,6 +20,7 @@ import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.staking.GetStakingIntegrationIdUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -52,6 +53,7 @@ internal class TokenDetailsStateFactory(
private val stakingEntryInfoProvider: Provider<StakingEntryInfo?>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val pendingBalancesProvider: Provider<List<BalanceItem>>,
private val clickIntents: TokenDetailsClickIntents,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
@ -82,6 +84,7 @@ internal class TokenDetailsStateFactory(
appCurrencyProvider = appCurrencyProvider,
stakingEntryInfoProvider = stakingEntryInfoProvider,
stakingAvailabilityProvider = stakingAvailabilityProvider,
pendingBalancesProvider = pendingBalancesProvider,
symbol = symbol,
decimals = decimals,
clickIntents = clickIntents,

View file

@ -32,12 +32,14 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.GetStakingPendingTransactionsUseCase
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.GetStakingIntegrationIdUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo
@ -119,6 +121,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val swapTransactionStatusStore: SwapTransactionStatusStore,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val associateAssetUseCase: AssociateAssetUseCase,
private val getStakingPendingTransactionsUseCase: GetStakingPendingTransactionsUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val vibratorHapticManager: VibratorHapticManager,
@ -148,6 +151,8 @@ internal class TokenDetailsViewModel @Inject constructor(
private val warningsJobHolder = JobHolder()
private val swapTxJobHolder = JobHolder()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private val stakingPendingBalances: List<BalanceItem>
get() = getStakingPendingTransactionsUseCase(userWalletId).getOrElse { emptyList() }
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var stakingEntryInfo: StakingEntryInfo? = null
@ -160,6 +165,7 @@ internal class TokenDetailsViewModel @Inject constructor(
stakingEntryInfoProvider = Provider { stakingEntryInfo },
stakingAvailabilityProvider = Provider { stakingAvailability },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
pendingBalancesProvider = Provider { stakingPendingBalances },
clickIntents = this,
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,

View file

@ -89,11 +89,11 @@ markdownComposeView = "0.5.4"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-805"
tangemBlockchainSdk = "develop-811"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-385"
tangemCardSdk = "develop-387"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem16"
tangemVico = "2.0.0-alpha.25-tangem17"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem

View file

@ -126,10 +126,14 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"sei-network" -> Blockchain.Sei
"sei-network/test" -> Blockchain.SeiTestnet
"internet-computer" -> Blockchain.InternetComputer
"sui" -> Blockchain.Sui
"sui/test" -> Blockchain.SuiTestnet
"energy-web-chain" -> Blockchain.EnergyWebChain
"energy-web-chain/test" -> Blockchain.EnergyWebChainTestnet
"energy-web-x" -> Blockchain.EnergyWebX
"energy-web-x/test" -> Blockchain.EnergyWebXTestnet
"add_later" -> Blockchain.Casper
"add_later_test" -> Blockchain.CasperTestnet
else -> null
}
}
@ -257,10 +261,14 @@ fun Blockchain.toNetworkId(): String {
Blockchain.Sei -> "sei-network"
Blockchain.SeiTestnet -> "sei-network/test"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.Sui -> "sui"
Blockchain.SuiTestnet -> "sui/test"
Blockchain.EnergyWebChain -> "energy-web-chain"
Blockchain.EnergyWebChainTestnet -> "energy-web-chain/test"
Blockchain.EnergyWebX -> "energy-web-x"
Blockchain.EnergyWebXTestnet -> "energy-web-x/test"
Blockchain.Casper -> "add_later"
Blockchain.CasperTestnet -> "add_later_test"
}
}
@ -345,8 +353,11 @@ fun Blockchain.toCoinId(): String {
Blockchain.Cyber, Blockchain.CyberTestnet -> "cyber-ethereum"
Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.Sui, Blockchain.SuiTestnet -> "sui"
Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet -> "energy-web-token"
Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet -> "energy-web-token"
Blockchain.Casper -> "add_later"
Blockchain.CasperTestnet -> "add_later_test"
}
}
@ -377,6 +388,5 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Nexa,
Blockchain.Sei,
Blockchain.InternetComputer,
Blockchain.NexaTestnet,
)