diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index c4760bb8a5..5132421c98 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -404,6 +404,7 @@ 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. Wert kopiert Meine Wallet + Warnung Woche mit Ja @@ -693,7 +694,7 @@ Lege die Karte oder Ring an Deine Wallet ist eingerichtet und einsatzbereit! Wallet erfolgreich importiert - Wiederherstellung %d% + Wiederherstellung %d%% Du hast deine biometrischen Daten aktualisiert, scanne deine Karte oder Ring, um einzutreten Dein Guthaben sollte höher sein als der Gebührenwert, um eine Überweisung zu tätigen Unzureichende Mittel diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 4fde82b922..1b40ed9ebf 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -401,6 +401,7 @@ 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. Valor copiado Billeteras + Alerta semana con diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 89c9435c9b..772f4d92d2 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -400,6 +400,7 @@ Unstakez 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. Valeur copiée + Alerte semaine avec Oui diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 9cd39c1c7a..0282ef7661 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -26,6 +26,7 @@ %d gettone %d gettoni + Avviso Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 6efdf8cd26..6dadf3d9d5 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -301,6 +301,7 @@ 時間 インポート 進行中 + 残高不足 後で もっと詳しく 残り%1$s @@ -397,6 +398,7 @@ %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 値がコピーされました ウォレット + 警告 はい @@ -473,7 +475,7 @@ 絞り込み マイネットワーク ネットワーク - よく使われています + 人気 該当する結果はありません ステーキング・利息モード こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 @@ -687,7 +689,7 @@ カードまたはリングをタップ ウォレットは同期され、準備ができています。\n一部のトークンが見つかりませんか? ウォレットのインポートが完了しました - %d%を復元中 + %d%%を復元中 生体認証を更新しました。カードまたはリングをスキャンして入ってください。 送金するには、残高が手数料額より高くなければなりません。 残高不足 diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 04ca28b1d4..c6642f4bfa 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -306,6 +306,7 @@ hora Importar Em andamento + Saldo insuficiente Mais tarde Saber mais %1$s falta @@ -404,6 +405,7 @@ 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. Valor copiado Carteiras + Aviso semana com Sim @@ -691,6 +693,9 @@ Toque para digitalizar Toque para assinar Toque no Cartão ou no Anel + Sua carteira está sincronizada e pronta. \n Faltam alguns tokens? + Carteira importada com sucesso. + Restaurando %d%% Você atualizou seus dados biométricos. Escaneie seu cartão ou toque a campainha para entrar. Seu saldo deve ser superior ao valor da taxa para efetuar uma transferência. Saldo insuficiente @@ -751,6 +756,7 @@ Resultado Veja tokens com capitalização de mercado inferior a 100 mil dólares. Mostrar tokens + Criptomoedas, notícias e muito mais Nenhum resultado Selecione a rede Selecione a carteira @@ -823,9 +829,11 @@ Posição no ranking de criptomoedas entre todas as moedas com base na capitalização de mercado. Posicionamento de mercado Fornecimento máximo + Circulação e Fornecimento Máximo O número máximo de moedas ou tokens que podem existir para uma determinada criptomoeda. Fornecimento máximo Métricas + Sem limite Links oficiais Desempenho de preço Repositório @@ -1100,6 +1108,12 @@ Configurações Você não concedeu acesso à sua câmera. Acesso à câmera negado + O token solicitado não foi adicionado à sua carteira. Adicione-o e tente novamente. + Token não adicionado + Desculpe, este código QR não pôde ser reconhecido. + Código QR não reconhecido + Esta rede não é compatível com nenhum dos tokens adicionados. Adicione um token compatível para enviar criptomoedas. + Nenhum token compatível encontrado Não é necessário memorando %1$s (%2$s) sobre %3$s rede %1$s sobre %2$s rede @@ -1245,6 +1259,11 @@ Limitação de transação Opcional Alinhe o seu código QR com o quadrado para escaneá-lo. Certifique-se de escanear corretamente. %s endereço de rede. + 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. + Taxa fixa + 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. + Taxa flutuante + A taxa é fixa. Recente Destinatário Endereço inválido @@ -1288,6 +1307,7 @@ O destinatário recebe %s Tem certeza de que deseja cancelar a conversão? Seus dados anteriores serão apagados. Remover conversão + Algo deu errado. Tente novamente. Enviar com troca Transação enviada Prepare-se para escanear o Cartão ou Anel que deseja configurar. @@ -1481,6 +1501,7 @@ Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado. Alto impacto nos preços Fundos insuficientes + Não há fundos suficientes para concluir esta transação. Reduza o valor a receber ou adicione mais fundos. Conceder permissão Trocar Trocar... @@ -1647,6 +1668,7 @@ Token em %%imagem%% %1$s rede 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 Não foi possível ocultar %s + N/A Mostrar código QR Troque este token por outro em %1$s taxas de serviço a partir de fevereiro %2$s-%3$s. Trocar com Changelly, %s tarifas diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index a624ed8ccd..2654c8ae24 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -415,6 +415,7 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Значение скопировано Кошельки + Предупреждение неделю с Да diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 14557017ad..67cdf8af99 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -415,6 +415,7 @@ Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. Скопійовано Гаманці + Увага тиждень з Так diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml index be6b9c60c8..9a07214403 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -6,7 +6,7 @@ 解鎖 您的錢包已同步並準備就緒。\n缺少某些代幣? 錢包導入成功 - 恢復%d% + 恢復%d%% diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index c410e833bc..c18d1cec74 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -99,6 +99,7 @@ 我了解 無法觸達 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。 + 警告 已複製代幣地址 支持的網路 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5f645487de..fe0d7fd0a8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -405,6 +405,7 @@ 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. Value copied Wallets + Warning week with Yes @@ -481,7 +482,7 @@ Filter by My networks Networks - Mostly used + Popular No results Staking & Yield mode Hi support team, I\'ve encountered an error with code: %s @@ -695,7 +696,7 @@ Tap the card or ring Your wallet is synced and ready.\nSome tokens missing? Wallet successfully imported - Restoring %d% + Restoring %d%% You have updated biometrics, scan your card or ring to enter Your balance should be higher than the fee value to make a transfer Not enough balance @@ -703,7 +704,7 @@ Not enough Mana You can transfer only %s due to the Mana limit imposed by the Koinos network Mana limit - The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana + The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana Mana level To begin tracking your crypto assets and transactions, add tokens Manage tokens @@ -752,7 +753,10 @@ No data Market Pulse Quick actions + Clear all Search tokens + Recents + In your portfolio Result See tokens under 100k USD market cap Show tokens @@ -829,9 +833,11 @@ Position in crypto rating between all coins based on market capitalization Market position Max supply + Circulating and Max Supply The maximum number of coins or tokens that can ever exist for a particular cryptocurrency Max supply Metrics + No limited Official links Price performance Repository @@ -1112,6 +1118,8 @@ Unrecognized QR Code This network isn\'t supported by any of your added tokens. Add a supported token to send crypto. No supported tokens found + This QR code contains parameters that are not recognized: %s. Some payment details may be lost if you continue. + Unknown Parameters No memo required %1$s (%2$s) on %3$s network %1$s on %2$s network @@ -1497,6 +1505,7 @@ Fee estimation error. Please send feedback to support. You swap Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. + You may receive significantly less due to low liquidity. Try a smaller amount or another provider. High price impact Insufficient funds Not enough funds to complete this transaction. Reduce the amount to receive or add more funds. @@ -1506,6 +1515,8 @@ You receive Choose token not available + Not enough liquidity for this trade.\nReduce the amount or choose another provider. + Trade too large We would be happy to receive your feedback Tangem Pay is now in beta Card frozen @@ -1666,6 +1677,7 @@ Token in %%image%% %1$s network 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 Unable to hide %s + N/A Show QR code Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Bip321PaymentUriParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Bip321PaymentUriParser.kt index 8e9dadca89..66a7d643a6 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Bip321PaymentUriParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Bip321PaymentUriParser.kt @@ -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, diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Eip681PaymentUriParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Eip681PaymentUriParser.kt index c4c656be84..69e2c408b8 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Eip681PaymentUriParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/Eip681PaymentUriParser.kt @@ -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, functionName: String?): Map { + 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, allCurrencies: List, ): 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, allCurrencies: List, ): 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, ) + 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 = setOf(VALUE.key, CHAIN_ID.key) + + /** Params supported for ERC-20 transfer() calls. */ + fun transferParams(): Set = setOf(ADDRESS.key, UINT256.key, CHAIN_ID.key) + } + } + private companion object { // ethereum:
[@][/][?] 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 diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriParser.kt index e7a64b8740..a74168e99b 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriParser.kt @@ -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, + ) : ParseResult() } } \ No newline at end of file diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriResolveHelper.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriResolveHelper.kt index c28f09b726..3df547b4b1 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriResolveHelper.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/PaymentUriResolveHelper.kt @@ -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, + memo: Pair?, + matchingCoins: List, + ): 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): Boolean { + return matchingCoins.any { + it.network.transactionExtrasType != Network.TransactionExtrasType.NONE + } + } + data class ResolveContext( val parsed: QrSentUriParser.Result, val matchingCoins: List, diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt index 3f8817247d..ca6fed1fc2 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt @@ -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 } diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrSentUriParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrSentUriParser.kt index af64a3c485..2655f9ee3b 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrSentUriParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrSentUriParser.kt @@ -8,8 +8,8 @@ internal class QrSentUriParser { data class Result( val address: String, val amount: BigDecimal?, - val memo: String?, - val params: Map, + val memo: Pair?, + val remainingParams: Map, ) 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" diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/SolanaPaymentUriParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/SolanaPaymentUriParser.kt index e3a85ded0a..cd9cee1f8c 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/SolanaPaymentUriParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/SolanaPaymentUriParser.kt @@ -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 { diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/TronPaymentUriParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/TronPaymentUriParser.kt index bbf95cf345..985e94855d 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/TronPaymentUriParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/TronPaymentUriParser.kt @@ -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, + ) } /** diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index 33d6158669..8b71bc156a 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -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() diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt index 4d3c46bdec..cac610a9e7 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt @@ -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, ) } diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt index e76d3c6910..a98cbe0026 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt @@ -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) diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt index 22d0e53a40..068663caed 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt @@ -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") diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt index 2525689756..8c07742046 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt @@ -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") diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/ClassifiedQrContent.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/ClassifiedQrContent.kt index 1fefc8a555..b9b7b63ff6 100644 --- a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/ClassifiedQrContent.kt +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/ClassifiedQrContent.kt @@ -19,6 +19,11 @@ sealed interface ClassifiedQrContent { val matchingCurrencies: List, ) : ClassifiedQrContent + data class PaymentUriWarning( + val paymentUri: PaymentUri, + val unsupportedParams: Map, + ) : ClassifiedQrContent + sealed interface Error : ClassifiedQrContent { /** QR code not recognized by any parser */ diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt index dcb02f58d4..986824f436 100644 --- a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt @@ -41,6 +41,11 @@ sealed class QrSendTarget { data object AddressSameAsWallet : QrSendTarget() + data class Warning( + val target: QrSendTarget, + val unsupportedParams: Map, + ) : QrSendTarget() + data class WalletConnect(val uri: String) : QrSendTarget() data class Error(val error: ClassifiedQrContent.Error) : QrSendTarget() diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt index 9353df0a2a..4156a83e1f 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt @@ -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, + ) + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index a65941d7ed..280c099e2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -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) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt index 9b07b7f428..8aa4bdf4a1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt @@ -94,6 +94,24 @@ internal object WalletAlertUM { ) } + fun qrCodeUnsupportedParams(unsupportedParams: Map, 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),