Updated on 2026-08-14
This commit is contained in:
parent
5e7e57b942
commit
1ec1196e12
29 changed files with 466 additions and 50 deletions
|
|
@ -404,6 +404,7 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren.</string>
|
||||
<string name="common_value_copied">Wert kopiert</string>
|
||||
<string name="common_wallets">Meine Wallet</string>
|
||||
<string name="common_warning">Warnung</string>
|
||||
<string name="common_week">Woche</string>
|
||||
<string name="common_with">mit</string>
|
||||
<string name="common_yes">Ja</string>
|
||||
|
|
@ -693,7 +694,7 @@
|
|||
<string name="initial_message_tap_header">Lege die Karte oder Ring an</string>
|
||||
<string name="initial_wallet_sync_banner_description">Deine Wallet ist eingerichtet und einsatzbereit!</string>
|
||||
<string name="initial_wallet_sync_banner_title">Wallet erfolgreich importiert</string>
|
||||
<string name="initial_wallet_sync_restore_progress">Wiederherstellung %d%</string>
|
||||
<string name="initial_wallet_sync_restore_progress">Wiederherstellung %d%%</string>
|
||||
<string name="key_invalidated_warning_description">Du hast deine biometrischen Daten aktualisiert, scanne deine Karte oder Ring, um einzutreten</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_description">Dein Guthaben sollte höher sein als der Gebührenwert, um eine Überweisung zu tätigen</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_title">Unzureichende Mittel</string>
|
||||
|
|
|
|||
|
|
@ -401,6 +401,7 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad.</string>
|
||||
<string name="common_value_copied">Valor copiado</string>
|
||||
<string name="common_wallets">Billeteras</string>
|
||||
<string name="common_warning">Alerta</string>
|
||||
<string name="common_week">semana</string>
|
||||
<string name="common_with">con</string>
|
||||
<string name="common_yes">Sí</string>
|
||||
|
|
|
|||
|
|
@ -400,6 +400,7 @@
|
|||
<string name="common_unstake">Unstakez</string>
|
||||
<string name="common_utxo_validate_withdrawal_message_warning">En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant.</string>
|
||||
<string name="common_value_copied">Valeur copiée</string>
|
||||
<string name="common_warning">Alerte</string>
|
||||
<string name="common_week">semaine</string>
|
||||
<string name="common_with">avec</string>
|
||||
<string name="common_yes">Oui</string>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
<item quantity="one">%d gettone</item>
|
||||
<item quantity="other">%d gettoni</item>
|
||||
</plurals>
|
||||
<string name="common_warning">Avviso</string>
|
||||
<string name="details_manage_security_access_code">Codice di accesso</string>
|
||||
<string name="details_manage_security_access_code_description">Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto</string>
|
||||
<string name="details_manage_security_long_tap">Mantenimento della carta</string>
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@
|
|||
<string name="common_hour">時間</string>
|
||||
<string name="common_import">インポート</string>
|
||||
<string name="common_in_progress">進行中</string>
|
||||
<string name="common_insufficient_balance">残高不足</string>
|
||||
<string name="common_later">後で</string>
|
||||
<string name="common_learn_more">もっと詳しく</string>
|
||||
<string name="common_left">残り%1$s</string>
|
||||
|
|
@ -397,6 +398,7 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">%1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。</string>
|
||||
<string name="common_value_copied">値がコピーされました</string>
|
||||
<string name="common_wallets">ウォレット</string>
|
||||
<string name="common_warning">警告</string>
|
||||
<string name="common_week">週</string>
|
||||
<string name="common_with">で</string>
|
||||
<string name="common_yes">はい</string>
|
||||
|
|
@ -473,7 +475,7 @@
|
|||
<string name="earn_filter_by">絞り込み</string>
|
||||
<string name="earn_filter_my_networks">マイネットワーク</string>
|
||||
<string name="earn_filter_networks">ネットワーク</string>
|
||||
<string name="earn_mostly_used">よく使われています</string>
|
||||
<string name="earn_mostly_used">人気</string>
|
||||
<string name="earn_no_results">該当する結果はありません</string>
|
||||
<string name="earn_title">ステーキング・利息モード</string>
|
||||
<string name="email_preface_wc_error">こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。</string>
|
||||
|
|
@ -687,7 +689,7 @@
|
|||
<string name="initial_message_tap_header">カードまたはリングをタップ</string>
|
||||
<string name="initial_wallet_sync_banner_description">ウォレットは同期され、準備ができています。\n一部のトークンが見つかりませんか?</string>
|
||||
<string name="initial_wallet_sync_banner_title">ウォレットのインポートが完了しました</string>
|
||||
<string name="initial_wallet_sync_restore_progress">%d%を復元中</string>
|
||||
<string name="initial_wallet_sync_restore_progress">%d%%を復元中</string>
|
||||
<string name="key_invalidated_warning_description">生体認証を更新しました。カードまたはリングをスキャンして入ってください。</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_description">送金するには、残高が手数料額より高くなければなりません。</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_title">残高不足</string>
|
||||
|
|
|
|||
|
|
@ -306,6 +306,7 @@
|
|||
<string name="common_hour">hora</string>
|
||||
<string name="common_import">Importar</string>
|
||||
<string name="common_in_progress">Em andamento</string>
|
||||
<string name="common_insufficient_balance">Saldo insuficiente</string>
|
||||
<string name="common_later">Mais tarde</string>
|
||||
<string name="common_learn_more">Saber mais</string>
|
||||
<string name="common_left">%1$s falta</string>
|
||||
|
|
@ -404,6 +405,7 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Devido a %1$s limitações apenas %2$d Os UTXOs podem caber em uma única transação. Isso significa que você só pode enviar %3$s ou menos. Você precisa reduzir a quantidade.</string>
|
||||
<string name="common_value_copied">Valor copiado</string>
|
||||
<string name="common_wallets">Carteiras</string>
|
||||
<string name="common_warning">Aviso</string>
|
||||
<string name="common_week">semana</string>
|
||||
<string name="common_with">com</string>
|
||||
<string name="common_yes">Sim</string>
|
||||
|
|
@ -691,6 +693,9 @@
|
|||
<string name="initial_message_scan_header">Toque para digitalizar</string>
|
||||
<string name="initial_message_sign_header">Toque para assinar</string>
|
||||
<string name="initial_message_tap_header">Toque no Cartão ou no Anel</string>
|
||||
<string name="initial_wallet_sync_banner_description">Sua carteira está sincronizada e pronta. \n Faltam alguns tokens?</string>
|
||||
<string name="initial_wallet_sync_banner_title">Carteira importada com sucesso.</string>
|
||||
<string name="initial_wallet_sync_restore_progress">Restaurando %d%%</string>
|
||||
<string name="key_invalidated_warning_description">Você atualizou seus dados biométricos. Escaneie seu cartão ou toque a campainha para entrar.</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_description">Seu saldo deve ser superior ao valor da taxa para efetuar uma transferência.</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_title">Saldo insuficiente</string>
|
||||
|
|
@ -751,6 +756,7 @@
|
|||
<string name="markets_search_result_title">Resultado</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Veja tokens com capitalização de mercado inferior a 100 mil dólares.</string>
|
||||
<string name="markets_search_show_tokens">Mostrar tokens</string>
|
||||
<string name="markets_search_title_placeholder">Criptomoedas, notícias e muito mais</string>
|
||||
<string name="markets_search_token_no_result_title">Nenhum resultado</string>
|
||||
<string name="markets_select_network">Selecione a rede</string>
|
||||
<string name="markets_select_wallet">Selecione a carteira</string>
|
||||
|
|
@ -823,9 +829,11 @@
|
|||
<string name="markets_token_details_market_rating_description">Posição no ranking de criptomoedas entre todas as moedas com base na capitalização de mercado.</string>
|
||||
<string name="markets_token_details_market_rating_full">Posicionamento de mercado</string>
|
||||
<string name="markets_token_details_max_supply">Fornecimento máximo</string>
|
||||
<string name="markets_token_details_max_supply_and_circulation_full">Circulação e Fornecimento Máximo</string>
|
||||
<string name="markets_token_details_max_supply_description">O número máximo de moedas ou tokens que podem existir para uma determinada criptomoeda.</string>
|
||||
<string name="markets_token_details_max_supply_full">Fornecimento máximo</string>
|
||||
<string name="markets_token_details_metrics">Métricas</string>
|
||||
<string name="markets_token_details_metrics_no_limited">Sem limite</string>
|
||||
<string name="markets_token_details_official_links">Links oficiais</string>
|
||||
<string name="markets_token_details_price_performance">Desempenho de preço</string>
|
||||
<string name="markets_token_details_repository">Repositório</string>
|
||||
|
|
@ -1100,6 +1108,12 @@
|
|||
<string name="qr_scanner_camera_denied_settings_button">Configurações</string>
|
||||
<string name="qr_scanner_camera_denied_text">Você não concedeu acesso à sua câmera.</string>
|
||||
<string name="qr_scanner_camera_denied_title">Acesso à câmera negado</string>
|
||||
<string name="qr_scanner_error_token_not_added_message">O token solicitado não foi adicionado à sua carteira. Adicione-o e tente novamente.</string>
|
||||
<string name="qr_scanner_error_token_not_added_title">Token não adicionado</string>
|
||||
<string name="qr_scanner_error_unrecognized_message">Desculpe, este código QR não pôde ser reconhecido.</string>
|
||||
<string name="qr_scanner_error_unrecognized_title">Código QR não reconhecido</string>
|
||||
<string name="qr_scanner_error_unsupported_network_message">Esta rede não é compatível com nenhum dos tokens adicionados. Adicione um token compatível para enviar criptomoedas.</string>
|
||||
<string name="qr_scanner_error_unsupported_network_title">Nenhum token compatível encontrado</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Não é necessário memorando</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) sobre %3$s rede</string>
|
||||
<string name="receive_bottom_sheet_warning_message_compact">%1$s sobre %2$s rede</string>
|
||||
|
|
@ -1245,6 +1259,11 @@
|
|||
<string name="send_notification_transaction_limit_title">Limitação de transação</string>
|
||||
<string name="send_optional_field">Opcional</string>
|
||||
<string name="send_qrcode_scan_info">Alinhe o seu código QR com o quadrado para escaneá-lo. Certifique-se de escanear corretamente. %s endereço de rede.</string>
|
||||
<string name="send_rate_fixed_info_description">Ao utilizar uma taxa fixa, o valor que você recebe fica garantido no momento da troca. Isso protege você de variações de preço durante a transação.</string>
|
||||
<string name="send_rate_fixed_info_title">Taxa fixa</string>
|
||||
<string name="send_rate_floating_info_description">Uma taxa flutuante significa que o valor final que você recebe pode variar ligeiramente com base nas condições de mercado entre o momento em que você inicia e conclui a operação de swap.</string>
|
||||
<string name="send_rate_floating_info_title">Taxa flutuante</string>
|
||||
<string name="send_rate_is_fixed">A taxa é fixa.</string>
|
||||
<string name="send_recent_transactions">Recente</string>
|
||||
<string name="send_recipient">Destinatário</string>
|
||||
<string name="send_recipient_address_error">Endereço inválido</string>
|
||||
|
|
@ -1288,6 +1307,7 @@
|
|||
<string name="send_with_swap_recipient_get_amount">O destinatário recebe %s</string>
|
||||
<string name="send_with_swap_remove_convert_alert_message">Tem certeza de que deseja cancelar a conversão? Seus dados anteriores serão apagados.</string>
|
||||
<string name="send_with_swap_remove_convert_alert_title">Remover conversão</string>
|
||||
<string name="send_with_swap_something_went_wrong">Algo deu errado. Tente novamente.</string>
|
||||
<string name="send_with_swap_title">Enviar com troca</string>
|
||||
<string name="sent_transaction_sent_title">Transação enviada</string>
|
||||
<string name="settings_card_settings_footer">Prepare-se para escanear o Cartão ou Anel que deseja configurar.</string>
|
||||
|
|
@ -1481,6 +1501,7 @@
|
|||
<string name="swapping_high_price_impact_description">Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado.</string>
|
||||
<string name="swapping_high_price_impact_title">Alto impacto nos preços</string>
|
||||
<string name="swapping_insufficient_funds">Fundos insuficientes</string>
|
||||
<string name="swapping_insufficient_funds_description">Não há fundos suficientes para concluir esta transação. Reduza o valor a receber ou adicione mais fundos.</string>
|
||||
<string name="swapping_permission_header">Conceder permissão</string>
|
||||
<string name="swapping_swap_action">Trocar</string>
|
||||
<string name="swapping_swap_action_in_progress">Trocar...</string>
|
||||
|
|
@ -1647,6 +1668,7 @@
|
|||
<string name="token_details_token_type_subtitle_no_standard">Token em %%imagem%% %1$s rede</string>
|
||||
<string name="token_details_unable_hide_alert_message">O %1$s (%2$sO token ) é a principal moeda da plataforma. %3$s rede e não pode ser ocultada enquanto você tiver outros tokens dessa rede na lista</string>
|
||||
<string name="token_details_unable_hide_alert_title">Não foi possível ocultar %s</string>
|
||||
<string name="token_market_metrics_no_data">N/A</string>
|
||||
<string name="token_receive_show_qr_code_title">Mostrar código QR</string>
|
||||
<string name="token_swap_changelly_promotion_message">Troque este token por outro em %1$s taxas de serviço a partir de fevereiro %2$s-%3$s.</string>
|
||||
<string name="token_swap_changelly_promotion_title">Trocar com Changelly, %s tarifas</string>
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
|
||||
<string name="common_value_copied">Значение скопировано</string>
|
||||
<string name="common_wallets">Кошельки</string>
|
||||
<string name="common_warning">Предупреждение</string>
|
||||
<string name="common_week">неделю</string>
|
||||
<string name="common_with">с</string>
|
||||
<string name="common_yes">Да</string>
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму.</string>
|
||||
<string name="common_value_copied">Скопійовано</string>
|
||||
<string name="common_wallets">Гаманці</string>
|
||||
<string name="common_warning">Увага</string>
|
||||
<string name="common_week">тиждень</string>
|
||||
<string name="common_with">з</string>
|
||||
<string name="common_yes">Так</string>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<string name="common_unlock">解鎖</string>
|
||||
<string name="initial_wallet_sync_banner_description">您的錢包已同步並準備就緒。\n缺少某些代幣?</string>
|
||||
<string name="initial_wallet_sync_banner_title">錢包導入成功</string>
|
||||
<string name="initial_wallet_sync_restore_progress">恢復%d%</string>
|
||||
<string name="initial_wallet_sync_restore_progress">恢復%d%%</string>
|
||||
<string name="markets_token_details_insight_day_timeline">天</string>
|
||||
<string name="markets_token_details_insight_month_timeline">月</string>
|
||||
<string name="markets_token_details_insight_week_timeline">週</string>
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
<string name="common_understand">我了解</string>
|
||||
<string name="common_unreachable">無法觸達</string>
|
||||
<string name="common_utxo_validate_withdrawal_message_warning">由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。</string>
|
||||
<string name="common_warning">警告</string>
|
||||
<string name="common_yes">是</string>
|
||||
<string name="contract_address_copied_message">已複製代幣地址</string>
|
||||
<string name="currency_subtitle_expanded">支持的網路</string>
|
||||
|
|
|
|||
|
|
@ -405,6 +405,7 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
|
||||
<string name="common_value_copied">Value copied</string>
|
||||
<string name="common_wallets">Wallets</string>
|
||||
<string name="common_warning">Warning</string>
|
||||
<string name="common_week">week</string>
|
||||
<string name="common_with">with</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
|
|
@ -481,7 +482,7 @@
|
|||
<string name="earn_filter_by">Filter by</string>
|
||||
<string name="earn_filter_my_networks">My networks</string>
|
||||
<string name="earn_filter_networks">Networks</string>
|
||||
<string name="earn_mostly_used">Mostly used</string>
|
||||
<string name="earn_mostly_used">Popular</string>
|
||||
<string name="earn_no_results">No results</string>
|
||||
<string name="earn_title">Staking & Yield mode</string>
|
||||
<string name="email_preface_wc_error">Hi support team, I\'ve encountered an error with code: %s</string>
|
||||
|
|
@ -695,7 +696,7 @@
|
|||
<string name="initial_message_tap_header">Tap the card or ring</string>
|
||||
<string name="initial_wallet_sync_banner_description">Your wallet is synced and ready.\nSome tokens missing?</string>
|
||||
<string name="initial_wallet_sync_banner_title">Wallet successfully imported</string>
|
||||
<string name="initial_wallet_sync_restore_progress">Restoring %d%</string>
|
||||
<string name="initial_wallet_sync_restore_progress">Restoring %d%%</string>
|
||||
<string name="key_invalidated_warning_description">You have updated biometrics, scan your card or ring to enter</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_description">Your balance should be higher than the fee value to make a transfer</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_title">Not enough balance</string>
|
||||
|
|
@ -703,7 +704,7 @@
|
|||
<string name="koinos_insufficient_mana_to_send_koin_title">Not enough Mana</string>
|
||||
<string name="koinos_mana_exceeds_koin_balance_description">You can transfer only %s due to the Mana limit imposed by the Koinos network</string>
|
||||
<string name="koinos_mana_exceeds_koin_balance_title">Mana limit</string>
|
||||
<string name="koinos_mana_level_description">The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana</string>
|
||||
<string name="koinos_mana_level_description">The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana</string>
|
||||
<string name="koinos_mana_level_title">Mana level</string>
|
||||
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string>
|
||||
<string name="main_manage_tokens">Manage tokens</string>
|
||||
|
|
@ -752,7 +753,10 @@
|
|||
<string name="markets_loading_no_data_title">No data</string>
|
||||
<string name="markets_pulse_common_title">Market Pulse</string>
|
||||
<string name="markets_quick_actions">Quick actions</string>
|
||||
<string name="markets_search_clear_all_hints">Clear all</string>
|
||||
<string name="markets_search_header_title">Search tokens</string>
|
||||
<string name="markets_search_hint_header">Recents</string>
|
||||
<string name="markets_search_portfolio_header">In your portfolio</string>
|
||||
<string name="markets_search_result_title">Result</string>
|
||||
<string name="markets_search_see_tokens_under_100k">See tokens under 100k USD market cap</string>
|
||||
<string name="markets_search_show_tokens">Show tokens</string>
|
||||
|
|
@ -829,9 +833,11 @@
|
|||
<string name="markets_token_details_market_rating_description">Position in crypto rating between all coins based on market capitalization</string>
|
||||
<string name="markets_token_details_market_rating_full">Market position</string>
|
||||
<string name="markets_token_details_max_supply">Max supply</string>
|
||||
<string name="markets_token_details_max_supply_and_circulation_full">Circulating and Max Supply</string>
|
||||
<string name="markets_token_details_max_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string>
|
||||
<string name="markets_token_details_max_supply_full">Max supply</string>
|
||||
<string name="markets_token_details_metrics">Metrics</string>
|
||||
<string name="markets_token_details_metrics_no_limited">No limited</string>
|
||||
<string name="markets_token_details_official_links">Official links</string>
|
||||
<string name="markets_token_details_price_performance">Price performance</string>
|
||||
<string name="markets_token_details_repository">Repository</string>
|
||||
|
|
@ -1112,6 +1118,8 @@
|
|||
<string name="qr_scanner_error_unrecognized_title">Unrecognized QR Code</string>
|
||||
<string name="qr_scanner_error_unsupported_network_message">This network isn\'t supported by any of your added tokens. Add a supported token to send crypto.</string>
|
||||
<string name="qr_scanner_error_unsupported_network_title">No supported tokens found</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_message">This QR code contains parameters that are not recognized: %s. Some payment details may be lost if you continue.</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_title">Unknown Parameters</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">No memo required</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) on %3$s network</string>
|
||||
<string name="receive_bottom_sheet_warning_message_compact">%1$s on %2$s network</string>
|
||||
|
|
@ -1497,6 +1505,7 @@
|
|||
<string name="swapping_fee_estimation_error_text">Fee estimation error. Please send feedback to support.</string>
|
||||
<string name="swapping_from_title">You swap</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_high_price_impact_text">You may receive significantly less due to low liquidity. Try a smaller amount or another provider.</string>
|
||||
<string name="swapping_high_price_impact_title">High price impact</string>
|
||||
<string name="swapping_insufficient_funds">Insufficient funds</string>
|
||||
<string name="swapping_insufficient_funds_description">Not enough funds to complete this transaction. Reduce the amount to receive or add more funds.</string>
|
||||
|
|
@ -1506,6 +1515,8 @@
|
|||
<string name="swapping_to_title">You receive</string>
|
||||
<string name="swapping_token_list_title">Choose token</string>
|
||||
<string name="swapping_token_not_available">not available</string>
|
||||
<string name="swapping_trade_too_large_text">Not enough liquidity for this trade.\nReduce the amount or choose another provider.</string>
|
||||
<string name="swapping_trade_too_large_title">Trade too large</string>
|
||||
<string name="tangem_pay_beta_notification_subtitle">We would be happy to receive your feedback</string>
|
||||
<string name="tangem_pay_beta_notification_title">Tangem Pay is now in beta</string>
|
||||
<string name="tangem_pay_card_frozen">Card frozen</string>
|
||||
|
|
@ -1666,6 +1677,7 @@
|
|||
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s network</string>
|
||||
<string name="token_details_unable_hide_alert_message">The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list</string>
|
||||
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
|
||||
<string name="token_market_metrics_no_data">N/A</string>
|
||||
<string name="token_receive_show_qr_code_title">Show QR code</string>
|
||||
<string name="token_swap_changelly_promotion_message">Exchange this token for another at %1$s service fees from February %2$s-%3$s.</string>
|
||||
<string name="token_swap_changelly_promotion_title">Swap with Changelly, %s fees</string>
|
||||
|
|
|
|||
|
|
@ -46,17 +46,26 @@ internal class Bip321PaymentUriParser(
|
|||
)
|
||||
}
|
||||
|
||||
return PaymentUriParser.ParseResult.Success(
|
||||
val result = PaymentUriParser.ParseResult.Success(
|
||||
ClassifiedQrContent.PaymentUri(
|
||||
address = parsed.address,
|
||||
amount = parsed.amount,
|
||||
memo = parsed.memo,
|
||||
memo = parsed.memo?.second,
|
||||
matchingCurrencies = matchingCurrencies,
|
||||
),
|
||||
)
|
||||
|
||||
val unconsumed = parsed.remainingParams - PARAM_LABEL
|
||||
return helper.validateParams(
|
||||
result = result,
|
||||
unconsumedParams = unconsumed,
|
||||
memo = parsed.memo,
|
||||
matchingCoins = matchingCoins,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PARAM_LABEL = "label"
|
||||
val BLOCKCHAINS = listOf(
|
||||
Blockchain.Bitcoin,
|
||||
Blockchain.BitcoinTestnet,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ internal class Eip681PaymentUriParser(
|
|||
}
|
||||
|
||||
val result = if (parsed.functionName == FUNCTION_TRANSFER) {
|
||||
val recipient = parsed.params[PARAM_ADDRESS]
|
||||
val recipient = parsed.params[Param.ADDRESS.key]
|
||||
if (recipient == null) {
|
||||
return PaymentUriParser.ParseResult.RecognizedError(
|
||||
ClassifiedQrContent.Error.Unrecognized(qrCode),
|
||||
|
|
@ -59,16 +59,30 @@ internal class Eip681PaymentUriParser(
|
|||
} else {
|
||||
resolveNativeTransfer(parsed, matchingCoins, allCurrencies)
|
||||
}
|
||||
return if (result != null) {
|
||||
PaymentUriParser.ParseResult.Success(result)
|
||||
} else {
|
||||
PaymentUriParser.ParseResult.RecognizedError(
|
||||
if (result == null) {
|
||||
return PaymentUriParser.ParseResult.RecognizedError(
|
||||
ClassifiedQrContent.Error.UnsupportedNetwork(
|
||||
raw = qrCode,
|
||||
blockchain = matchingCoins.firstOrNull()?.network?.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val unsupportedParams = findUnsupportedParams(parsed.params, parsed.functionName)
|
||||
return if (unsupportedParams.isEmpty()) {
|
||||
PaymentUriParser.ParseResult.Success(result)
|
||||
} else {
|
||||
PaymentUriParser.ParseResult.SuccessWithWarning(result, unsupportedParams)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findUnsupportedParams(params: Map<String, String>, functionName: String?): Map<String, String> {
|
||||
val supportedKeys = if (functionName == FUNCTION_TRANSFER) {
|
||||
Param.transferParams()
|
||||
} else {
|
||||
Param.nativeParams()
|
||||
}
|
||||
return params.filterKeys { key -> key !in supportedKeys }
|
||||
}
|
||||
|
||||
private fun resolveNativeTransfer(
|
||||
|
|
@ -76,7 +90,7 @@ internal class Eip681PaymentUriParser(
|
|||
matchingCoins: List<CryptoCurrency.Coin>,
|
||||
allCurrencies: List<CryptoCurrency>,
|
||||
): ClassifiedQrContent.PaymentUri? {
|
||||
val valueWei = parsed.params[PARAM_VALUE]?.toBigDecimalOrNull()
|
||||
val valueWei = parsed.params[Param.VALUE.key]?.toBigDecimalOrNull()
|
||||
|
||||
if (matchingCoins.isEmpty()) return null
|
||||
|
||||
|
|
@ -105,7 +119,7 @@ internal class Eip681PaymentUriParser(
|
|||
matchingCoins: List<CryptoCurrency.Coin>,
|
||||
allCurrencies: List<CryptoCurrency>,
|
||||
): ClassifiedQrContent.PaymentUri? {
|
||||
val recipient = parsed.params[PARAM_ADDRESS] ?: return null
|
||||
val recipient = parsed.params[Param.ADDRESS.key] ?: return null
|
||||
val contractAddress = parsed.targetAddress
|
||||
|
||||
val matchingNetworkIds = matchingCoins.map { it.network.id }.toSet()
|
||||
|
|
@ -118,7 +132,7 @@ internal class Eip681PaymentUriParser(
|
|||
|
||||
if (matchingTokens.isEmpty()) return null
|
||||
|
||||
val rawAmount = parsed.params[PARAM_UINT256]?.toBigDecimalOrNull()
|
||||
val rawAmount = parsed.params[Param.UINT256.key]?.toBigDecimalOrNull()
|
||||
val amount = rawAmount?.fromSmallestUnit(matchingTokens.first().decimals)
|
||||
|
||||
return ClassifiedQrContent.PaymentUri(
|
||||
|
|
@ -149,7 +163,7 @@ internal class Eip681PaymentUriParser(
|
|||
val queryString = match.groupValues[GROUP_QUERY]
|
||||
|
||||
val params = parseQueryParams(queryString)
|
||||
val chainId = pathChainId ?: params[PARAM_CHAIN_ID]?.toLongOrNull()
|
||||
val chainId = pathChainId ?: params[Param.CHAIN_ID.key]?.toLongOrNull()
|
||||
|
||||
return Eip681Result(
|
||||
targetAddress = targetAddress,
|
||||
|
|
@ -179,15 +193,27 @@ internal class Eip681PaymentUriParser(
|
|||
val params: Map<String, String>,
|
||||
)
|
||||
|
||||
private enum class Param(val key: String) {
|
||||
VALUE("value"),
|
||||
ADDRESS("address"),
|
||||
UINT256("uint256"),
|
||||
CHAIN_ID("chainId"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
/** Params supported for native transfers (no function or unknown function). */
|
||||
fun nativeParams(): Set<String> = setOf(VALUE.key, CHAIN_ID.key)
|
||||
|
||||
/** Params supported for ERC-20 transfer() calls. */
|
||||
fun transferParams(): Set<String> = setOf(ADDRESS.key, UINT256.key, CHAIN_ID.key)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// ethereum:<address>[@<chainId>][/<function>][?<params>]
|
||||
val URI_REGEX = Regex("""^([^@/?]+)(?:@(\d+))?(?:/([^?]+))?(?:\?(.+))?$""")
|
||||
val SCHEMES = Blockchain.Ethereum.getShareScheme()
|
||||
const val FUNCTION_TRANSFER = "transfer"
|
||||
const val PARAM_VALUE = "value"
|
||||
const val PARAM_ADDRESS = "address"
|
||||
const val PARAM_UINT256 = "uint256"
|
||||
const val PARAM_CHAIN_ID = "chainId"
|
||||
const val GROUP_ADDRESS = 1
|
||||
const val GROUP_CHAIN_ID = 2
|
||||
const val GROUP_FUNCTION = 3
|
||||
|
|
|
|||
|
|
@ -16,5 +16,11 @@ internal interface PaymentUriParser {
|
|||
|
||||
/** Successfully parsed with matching currencies. */
|
||||
data class Success(val content: ClassifiedQrContent.PaymentUri) : ParseResult()
|
||||
|
||||
/** Successfully parsed but QR contains unsupported parameters. */
|
||||
data class SuccessWithWarning(
|
||||
val content: ClassifiedQrContent.PaymentUri,
|
||||
val unsupportedParams: Map<String, String>,
|
||||
) : ParseResult()
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ internal class PaymentUriResolveHelper {
|
|||
ClassifiedQrContent.PaymentUri(
|
||||
address = context.parsed.address,
|
||||
amount = amount,
|
||||
memo = context.parsed.memo,
|
||||
memo = context.parsed.memo?.second,
|
||||
matchingCurrencies = matchingTokens,
|
||||
),
|
||||
)
|
||||
|
|
@ -67,12 +67,40 @@ internal class PaymentUriResolveHelper {
|
|||
ClassifiedQrContent.PaymentUri(
|
||||
address = context.parsed.address,
|
||||
amount = amount,
|
||||
memo = context.parsed.memo,
|
||||
memo = context.parsed.memo?.second,
|
||||
matchingCurrencies = matchingCurrencies,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun validateParams(
|
||||
result: PaymentUriParser.ParseResult,
|
||||
unconsumedParams: Map<String, String>,
|
||||
memo: Pair<String, String>?,
|
||||
matchingCoins: List<CryptoCurrency.Coin>,
|
||||
): PaymentUriParser.ParseResult {
|
||||
if (result !is PaymentUriParser.ParseResult.Success) return result
|
||||
|
||||
val unsupported = buildMap {
|
||||
putAll(unconsumedParams)
|
||||
if (memo != null && !isMemoSupported(matchingCoins)) {
|
||||
put(memo.first, memo.second)
|
||||
}
|
||||
}
|
||||
|
||||
return if (unsupported.isEmpty()) {
|
||||
result
|
||||
} else {
|
||||
PaymentUriParser.ParseResult.SuccessWithWarning(result.content, unsupported)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMemoSupported(matchingCoins: List<CryptoCurrency.Coin>): Boolean {
|
||||
return matchingCoins.any {
|
||||
it.network.transactionExtrasType != Network.TransactionExtrasType.NONE
|
||||
}
|
||||
}
|
||||
|
||||
data class ResolveContext(
|
||||
val parsed: QrSentUriParser.Result,
|
||||
val matchingCoins: List<CryptoCurrency.Coin>,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ internal class QrContentClassifierParser(
|
|||
|
||||
when (val paymentUriResult = tryParsePaymentUri(qrCode, uniqueCoins, userCurrencies)) {
|
||||
is PaymentUriParser.ParseResult.Success -> return paymentUriResult.content
|
||||
is PaymentUriParser.ParseResult.SuccessWithWarning -> {
|
||||
return ClassifiedQrContent.PaymentUriWarning(
|
||||
paymentUri = paymentUriResult.content,
|
||||
unsupportedParams = paymentUriResult.unsupportedParams,
|
||||
)
|
||||
}
|
||||
is PaymentUriParser.ParseResult.RecognizedError -> return paymentUriResult.error
|
||||
is PaymentUriParser.ParseResult.NotRecognized -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ internal class QrSentUriParser {
|
|||
data class Result(
|
||||
val address: String,
|
||||
val amount: BigDecimal?,
|
||||
val memo: String?,
|
||||
val params: Map<String, String>,
|
||||
val memo: Pair<String, String>?,
|
||||
val remainingParams: Map<String, String>,
|
||||
)
|
||||
|
||||
fun parse(withoutScheme: String): Result? {
|
||||
|
|
@ -20,15 +20,24 @@ internal class QrSentUriParser {
|
|||
|
||||
val params = extractParameters(withoutScheme)
|
||||
val amount = params[PARAM_AMOUNT]?.toBigDecimalOrNull()
|
||||
val memo = (params[PARAM_MEMO] ?: params[PARAM_MESSAGE] ?: params[PARAM_DT])?.let {
|
||||
runCatching { URLDecoder.decode(it, CHARSET_UTF8) }.getOrDefault(it)
|
||||
|
||||
val memoKey = MemoParam.keys.firstOrNull { it in params }
|
||||
val memo = memoKey?.let { key ->
|
||||
val raw = params[key] ?: return@let null
|
||||
val decoded = runCatching { URLDecoder.decode(raw, CHARSET_UTF8) }.getOrDefault(raw)
|
||||
key to decoded
|
||||
}
|
||||
|
||||
val consumedKeys = buildSet {
|
||||
add(PARAM_AMOUNT)
|
||||
addAll(MemoParam.keys)
|
||||
}
|
||||
|
||||
return Result(
|
||||
address = address,
|
||||
amount = amount,
|
||||
memo = memo,
|
||||
params = params,
|
||||
remainingParams = params - consumedKeys,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +53,17 @@ internal class QrSentUriParser {
|
|||
.toMap()
|
||||
}
|
||||
|
||||
enum class MemoParam(val key: String) {
|
||||
MEMO("memo"),
|
||||
MESSAGE("message"),
|
||||
DESTINATION_TAG("dt"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val keys = entries.map { it.key }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHAIN_DELIMITER = '@'
|
||||
const val FUNCTION_DELIMITER = '/'
|
||||
|
|
@ -51,9 +71,6 @@ internal class QrSentUriParser {
|
|||
const val PARAMS_DELIMITER = '&'
|
||||
const val PARAM_VALUE_DELIMITER = '='
|
||||
const val PARAM_AMOUNT = "amount"
|
||||
const val PARAM_MEMO = "memo"
|
||||
const val PARAM_MESSAGE = "message"
|
||||
const val PARAM_DT = "dt"
|
||||
const val PARAM_ADDRESS = "address"
|
||||
const val PARAM_VALUE = "value"
|
||||
const val PARAM_UINT256 = "uint256"
|
||||
|
|
|
|||
|
|
@ -49,12 +49,20 @@ internal class SolanaPaymentUriParser(
|
|||
qrCode = qrCode,
|
||||
)
|
||||
|
||||
val splTokenMint = parsed.params[PARAM_SPL_TOKEN]
|
||||
return if (splTokenMint != null) {
|
||||
val splTokenMint = parsed.remainingParams[PARAM_SPL_TOKEN]
|
||||
val result = if (splTokenMint != null) {
|
||||
helper.resolveTokenTransfer(context, splTokenMint)
|
||||
} else {
|
||||
helper.resolveNativeOrAll(context)
|
||||
}
|
||||
|
||||
val unconsumed = parsed.remainingParams - PARAM_SPL_TOKEN
|
||||
return helper.validateParams(
|
||||
result = result,
|
||||
unconsumedParams = unconsumed,
|
||||
memo = parsed.memo,
|
||||
matchingCoins = matchingCoins,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -51,12 +51,20 @@ internal class TronPaymentUriParser(
|
|||
qrCode = qrCode,
|
||||
)
|
||||
|
||||
val tokenContractAddress = parsed.params[PARAM_TOKEN]
|
||||
return if (tokenContractAddress != null) {
|
||||
val tokenContractAddress = parsed.remainingParams[PARAM_TOKEN]
|
||||
val result = if (tokenContractAddress != null) {
|
||||
helper.resolveTokenTransfer(context, tokenContractAddress, ::interpretAmount)
|
||||
} else {
|
||||
helper.resolveNativeOrAll(context, ::interpretAmount)
|
||||
}
|
||||
|
||||
val unconsumed = parsed.remainingParams - PARAM_TOKEN
|
||||
return helper.validateParams(
|
||||
result = result,
|
||||
unconsumedParams = unconsumed,
|
||||
memo = parsed.memo,
|
||||
matchingCoins = matchingCoins,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ internal class DefaultQrScanningEventsRepository(
|
|||
|
||||
val result = QrResult(address = parsed.address)
|
||||
result.amount = parsed.amount
|
||||
result.memo = parsed.memo
|
||||
result.memo = parsed.memo?.second
|
||||
|
||||
// ERC-681: if 'address' parameter exists, currency must be a token,
|
||||
// and the URI address must match the token's contract address.
|
||||
parsed.params[QrSentUriParser.PARAM_ADDRESS]?.let { addressValue ->
|
||||
parsed.remainingParams[QrSentUriParser.PARAM_ADDRESS]?.let { addressValue ->
|
||||
val tokenCurrency = cryptoCurrency as? CryptoCurrency.Token ?: return QrResult()
|
||||
if (tokenCurrency.contractAddress.equals(parsed.address, ignoreCase = true)) {
|
||||
result.address = addressValue
|
||||
|
|
@ -61,8 +61,8 @@ internal class DefaultQrScanningEventsRepository(
|
|||
}
|
||||
|
||||
// ERC-681: value/uint256 is in the smallest unit, needs conversion
|
||||
val valueStr = parsed.params[QrSentUriParser.PARAM_VALUE]
|
||||
?: parsed.params[QrSentUriParser.PARAM_UINT256]
|
||||
val valueStr = parsed.remainingParams[QrSentUriParser.PARAM_VALUE]
|
||||
?: parsed.remainingParams[QrSentUriParser.PARAM_UINT256]
|
||||
if (valueStr != null) {
|
||||
result.amount = valueStr.parseBigDecimalOrNull()
|
||||
?.toPlainString()?.toBigDecimalOrNull()
|
||||
|
|
|
|||
|
|
@ -215,15 +215,76 @@ internal class Bip321PaymentUriParserTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `bitcoin URI with memo param`() {
|
||||
fun `bitcoin URI with memo param returns warning for unsupported memo`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=1&memo=TestMemo",
|
||||
coins = listOf(bitcoinCoin),
|
||||
allCurrencies = listOf(bitcoinCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.content.memo).isEqualTo("TestMemo")
|
||||
assertThat(warning.unsupportedParams).containsEntry("memo", "TestMemo")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Unsupported params
|
||||
|
||||
@Test
|
||||
fun `unknown parameter returns SuccessWithWarning`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.5&req-unknownparam=bar",
|
||||
coins = listOf(bitcoinCoin),
|
||||
allCurrencies = listOf(bitcoinCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.content.address).isEqualTo("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
|
||||
assertThat(warning.unsupportedParams).containsEntry("req-unknownparam", "bar")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple unknown parameters all reported`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=1&foo=1&bar=2",
|
||||
coins = listOf(bitcoinCoin),
|
||||
allCurrencies = listOf(bitcoinCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.unsupportedParams).hasSize(2)
|
||||
assertThat(warning.unsupportedParams).containsEntry("foo", "1")
|
||||
assertThat(warning.unsupportedParams).containsEntry("bar", "2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `memo on network with memo support is not unsupported`() {
|
||||
val xrpCoin = buildCoin("XRP", "XRP", "XRP", decimals = 6, extrasType = Network.TransactionExtrasType.DESTINATION_TAG)
|
||||
|
||||
val result = parser.parse(
|
||||
qrCode = "ripple:rAddress?dt=12345",
|
||||
coins = listOf(xrpCoin),
|
||||
allCurrencies = listOf(xrpCoin),
|
||||
).asSuccess()
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.memo).isEqualTo("12345")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only known params returns Success`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.5&label=Satoshi",
|
||||
coins = listOf(bitcoinCoin),
|
||||
allCurrencies = listOf(bitcoinCoin),
|
||||
).asSuccess()
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.memo).isEqualTo("TestMemo")
|
||||
assertThat(result!!.address).isEqualTo("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -231,7 +292,11 @@ internal class Bip321PaymentUriParserTest {
|
|||
// region Helpers
|
||||
|
||||
private fun PaymentUriParser.ParseResult.asSuccess(): ClassifiedQrContent.PaymentUri? {
|
||||
return (this as? PaymentUriParser.ParseResult.Success)?.content
|
||||
return when (this) {
|
||||
is PaymentUriParser.ParseResult.Success -> content
|
||||
is PaymentUriParser.ParseResult.SuccessWithWarning -> content
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val bitcoinCoin = buildCoin("BTC", "Bitcoin", "BTC", decimals = 8)
|
||||
|
|
@ -243,6 +308,7 @@ internal class Bip321PaymentUriParserTest {
|
|||
name: String,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
extrasType: Network.TransactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
): CryptoCurrency.Coin {
|
||||
return CryptoCurrency.Coin(
|
||||
id = CryptoCurrency.ID(
|
||||
|
|
@ -250,7 +316,7 @@ internal class Bip321PaymentUriParserTest {
|
|||
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId),
|
||||
),
|
||||
network = buildNetwork(rawNetworkId, name, symbol),
|
||||
network = buildNetwork(rawNetworkId, name, symbol, extrasType),
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
|
|
@ -276,7 +342,12 @@ internal class Bip321PaymentUriParserTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun buildNetwork(rawNetworkId: String, name: String, symbol: String): Network {
|
||||
private fun buildNetwork(
|
||||
rawNetworkId: String,
|
||||
name: String,
|
||||
symbol: String,
|
||||
extrasType: Network.TransactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
): Network {
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None),
|
||||
backendId = rawNetworkId,
|
||||
|
|
@ -287,7 +358,7 @@ internal class Bip321PaymentUriParserTest {
|
|||
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
transactionExtrasType = extrasType,
|
||||
nameResolvingType = Network.NameResolvingType.NONE,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,10 +283,64 @@ internal class Eip681PaymentUriParserTest {
|
|||
|
||||
// endregion
|
||||
|
||||
// region Unsupported params
|
||||
|
||||
@Test
|
||||
fun `native transfer with unknown param returns SuccessWithWarning`() {
|
||||
every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L
|
||||
|
||||
val result = parser.parse(
|
||||
qrCode = "ethereum:0xRecipient@1?value=1000000000000000000&gasLimit=21000",
|
||||
coins = listOf(ethereumCoin),
|
||||
allCurrencies = listOf(ethereumCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.content.address).isEqualTo("0xRecipient")
|
||||
assertThat(warning.unsupportedParams).containsEntry("gasLimit", "21000")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ERC-20 transfer with unknown param returns SuccessWithWarning`() {
|
||||
every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L
|
||||
|
||||
val usdcToken = buildToken("ethereum", "USDC", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
|
||||
|
||||
val result = parser.parse(
|
||||
qrCode = "ethereum:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48@1/transfer?address=0xRecipient&uint256=1000000&gasPrice=20000000000",
|
||||
coins = listOf(ethereumCoin),
|
||||
allCurrencies = listOf(ethereumCoin, usdcToken),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.unsupportedParams).containsEntry("gasPrice", "20000000000")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `native transfer with only known params returns Success`() {
|
||||
every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L
|
||||
|
||||
val result = parser.parse(
|
||||
qrCode = "ethereum:0xRecipient@1?value=1000000000000000000",
|
||||
coins = listOf(ethereumCoin),
|
||||
allCurrencies = listOf(ethereumCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.Success::class.java)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun PaymentUriParser.ParseResult.asSuccess(): ClassifiedQrContent.PaymentUri? {
|
||||
return (this as? PaymentUriParser.ParseResult.Success)?.content
|
||||
return when (this) {
|
||||
is PaymentUriParser.ParseResult.Success -> content
|
||||
is PaymentUriParser.ParseResult.SuccessWithWarning -> content
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val bitcoinCoin = buildCoin("bitcoin", decimals = 8)
|
||||
|
|
|
|||
|
|
@ -146,10 +146,56 @@ internal class SolanaPaymentUriParserTest {
|
|||
|
||||
// endregion
|
||||
|
||||
// region Unsupported params
|
||||
|
||||
@Test
|
||||
fun `unknown parameter returns SuccessWithWarning`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "solana:SolAddress?amount=1&reference=abc123",
|
||||
coins = listOf(solanaCoin),
|
||||
allCurrencies = listOf(solanaCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.content.address).isEqualTo("SolAddress")
|
||||
assertThat(warning.unsupportedParams).containsEntry("reference", "abc123")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `memo on non-memo network returns SuccessWithWarning`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "solana:SolAddress?amount=1&memo=hello",
|
||||
coins = listOf(solanaCoin),
|
||||
allCurrencies = listOf(solanaCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.unsupportedParams).containsEntry("memo", "hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only known params returns Success`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "solana:SolAddress?amount=1",
|
||||
coins = listOf(solanaCoin),
|
||||
allCurrencies = listOf(solanaCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.Success::class.java)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun PaymentUriParser.ParseResult.asSuccess(): ClassifiedQrContent.PaymentUri? {
|
||||
return (this as? PaymentUriParser.ParseResult.Success)?.content
|
||||
return when (this) {
|
||||
is PaymentUriParser.ParseResult.Success -> content
|
||||
is PaymentUriParser.ParseResult.SuccessWithWarning -> content
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val solanaNetwork = buildNetwork("SOLANA", "Solana", "SOL")
|
||||
|
|
|
|||
|
|
@ -186,10 +186,56 @@ internal class TronPaymentUriParserTest {
|
|||
|
||||
// endregion
|
||||
|
||||
// region Unsupported params
|
||||
|
||||
@Test
|
||||
fun `unknown parameter returns SuccessWithWarning`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "tron:TAddress?amount=100&gasLimit=21000",
|
||||
coins = listOf(tronCoin),
|
||||
allCurrencies = listOf(tronCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.content.address).isEqualTo("TAddress")
|
||||
assertThat(warning.unsupportedParams).containsEntry("gaslimit", "21000")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `memo on non-memo network returns SuccessWithWarning`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "tron:TAddress?amount=100&memo=hello",
|
||||
coins = listOf(tronCoin),
|
||||
allCurrencies = listOf(tronCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.SuccessWithWarning::class.java)
|
||||
val warning = result as PaymentUriParser.ParseResult.SuccessWithWarning
|
||||
assertThat(warning.unsupportedParams).containsEntry("memo", "hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only known params returns Success`() {
|
||||
val result = parser.parse(
|
||||
qrCode = "tron:TAddress?amount=100",
|
||||
coins = listOf(tronCoin),
|
||||
allCurrencies = listOf(tronCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.Success::class.java)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun PaymentUriParser.ParseResult.asSuccess(): ClassifiedQrContent.PaymentUri? {
|
||||
return (this as? PaymentUriParser.ParseResult.Success)?.content
|
||||
return when (this) {
|
||||
is PaymentUriParser.ParseResult.Success -> content
|
||||
is PaymentUriParser.ParseResult.SuccessWithWarning -> content
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val tronNetwork = buildNetwork("TRON", "Tron", "TRX")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ sealed interface ClassifiedQrContent {
|
|||
val matchingCurrencies: List<CryptoCurrency>,
|
||||
) : ClassifiedQrContent
|
||||
|
||||
data class PaymentUriWarning(
|
||||
val paymentUri: PaymentUri,
|
||||
val unsupportedParams: Map<String, String>,
|
||||
) : ClassifiedQrContent
|
||||
|
||||
sealed interface Error : ClassifiedQrContent {
|
||||
|
||||
/** QR code not recognized by any parser */
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ sealed class QrSendTarget {
|
|||
|
||||
data object AddressSameAsWallet : QrSendTarget()
|
||||
|
||||
data class Warning(
|
||||
val target: QrSendTarget,
|
||||
val unsupportedParams: Map<String, String>,
|
||||
) : QrSendTarget()
|
||||
|
||||
data class WalletConnect(val uri: String) : QrSendTarget()
|
||||
|
||||
data class Error(val error: ClassifiedQrContent.Error) : QrSendTarget()
|
||||
|
|
|
|||
|
|
@ -73,6 +73,13 @@ class ResolveQrSendTargetsUseCase(
|
|||
matchingCurrencies = classified.matchingCurrencies,
|
||||
portfolioIndex = portfolioIndex,
|
||||
)
|
||||
is ClassifiedQrContent.PaymentUriWarning -> {
|
||||
val inner = resolve(classified.paymentUri, portfolioIndex)
|
||||
QrSendTarget.Warning(
|
||||
target = inner,
|
||||
unsupportedParams = classified.unsupportedParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -755,7 +755,12 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun handleQrResult(qrCode: String, resultSource: QrResultSource) {
|
||||
when (val target = resolveQrSendTargetsUseCase(qrCode)) {
|
||||
val target = resolveQrSendTargetsUseCase(qrCode)
|
||||
handleQrTarget(target, resultSource)
|
||||
}
|
||||
|
||||
private fun handleQrTarget(target: QrSendTarget, resultSource: QrResultSource) {
|
||||
when (target) {
|
||||
is QrSendTarget.WalletConnect -> {
|
||||
val source = when (resultSource) {
|
||||
QrResultSource.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD
|
||||
|
|
@ -788,6 +793,14 @@ internal class WalletModel @Inject constructor(
|
|||
is QrSendTarget.AddressSameAsWallet -> {
|
||||
uiMessageSender.send(WalletAlertUM.qrCodeAddressSameAsWallet())
|
||||
}
|
||||
is QrSendTarget.Warning -> {
|
||||
uiMessageSender.send(
|
||||
WalletAlertUM.qrCodeUnsupportedParams(
|
||||
unsupportedParams = target.unsupportedParams,
|
||||
onContinue = { handleQrTarget(target.target, resultSource) },
|
||||
),
|
||||
)
|
||||
}
|
||||
is QrSendTarget.Error -> handleQrError(target.error)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,24 @@ internal object WalletAlertUM {
|
|||
)
|
||||
}
|
||||
|
||||
fun qrCodeUnsupportedParams(unsupportedParams: Map<String, String>, onContinue: () -> Unit): DialogMessage {
|
||||
val paramsList = unsupportedParams.entries.joinToString { "${it.key} = ${it.value}" }
|
||||
return DialogMessage(
|
||||
title = resourceReference(R.string.qr_scanner_warning_unknown_parameters_title),
|
||||
message = resourceReference(
|
||||
id = R.string.qr_scanner_warning_unknown_parameters_message,
|
||||
formatArgs = WrappedList(listOf(paramsList)),
|
||||
),
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.common_continue),
|
||||
onClick = onContinue,
|
||||
)
|
||||
},
|
||||
secondActionBuilder = { cancelAction() },
|
||||
)
|
||||
}
|
||||
|
||||
fun qrCodeAddressSameAsWallet(): DialogMessage {
|
||||
return DialogMessage(
|
||||
message = resourceReference(R.string.send_error_address_same_as_wallet),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue