diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt index b824e19a4e..76bd17a9b1 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt @@ -26,9 +26,9 @@ internal object QrScanningDataModule { blockchainDataProvider = blockchainDataProvider, paymentUriParsers = setOf( Eip681PaymentUriParser(blockchainDataProvider), - TronPaymentUriParser(), - SolanaPaymentUriParser(), - Bip321PaymentUriParser(), + TronPaymentUriParser(blockchainDataProvider), + SolanaPaymentUriParser(blockchainDataProvider), + Bip321PaymentUriParser(blockchainDataProvider), ), ), ) 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 ba281b2a7f..8e9dadca89 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 @@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.qrscanning.models.ClassifiedQrContent internal class Bip321PaymentUriParser( + private val blockchainDataProvider: QrContentClassifierParser.BlockchainDataProvider, private val helper: PaymentUriResolveHelper = PaymentUriResolveHelper(), ) : PaymentUriParser { @@ -36,6 +37,15 @@ internal class Bip321PaymentUriParser( ) } + val isAddressValid = matchingCoins.any { + blockchainDataProvider.validateAddress(it.network, parsed.address) + } + if (!isAddressValid) { + return PaymentUriParser.ParseResult.RecognizedError( + ClassifiedQrContent.Error.Unrecognized(qrCode), + ) + } + return PaymentUriParser.ParseResult.Success( ClassifiedQrContent.PaymentUri( address = parsed.address, 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 1a0fa0caac..c4c656be84 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 @@ -31,8 +31,26 @@ internal class Eip681PaymentUriParser( ) } + val isAddressValid = matchingCoins.any { coin -> + blockchainDataProvider.validateAddress(coin.network, parsed.targetAddress) + } + if (!isAddressValid) { + return PaymentUriParser.ParseResult.RecognizedError( + ClassifiedQrContent.Error.Unrecognized(qrCode), + ) + } + val result = if (parsed.functionName == FUNCTION_TRANSFER) { - if (PARAM_ADDRESS !in parsed.params) { + val recipient = parsed.params[PARAM_ADDRESS] + if (recipient == null) { + return PaymentUriParser.ParseResult.RecognizedError( + ClassifiedQrContent.Error.Unrecognized(qrCode), + ) + } + val isRecipientValid = matchingCoins.any { coin -> + blockchainDataProvider.validateAddress(coin.network, recipient) + } + if (!isRecipientValid) { return PaymentUriParser.ParseResult.RecognizedError( ClassifiedQrContent.Error.Unrecognized(qrCode), ) 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 5c00d4eab8..e3a85ded0a 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 @@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.qrscanning.models.ClassifiedQrContent internal class SolanaPaymentUriParser( + private val blockchainDataProvider: QrContentClassifierParser.BlockchainDataProvider, private val helper: PaymentUriResolveHelper = PaymentUriResolveHelper(), ) : PaymentUriParser { @@ -29,6 +30,15 @@ internal class SolanaPaymentUriParser( ) } + val isAddressValid = matchingCoins.any { + blockchainDataProvider.validateAddress(it.network, parsed.address) + } + if (!isAddressValid) { + return PaymentUriParser.ParseResult.RecognizedError( + ClassifiedQrContent.Error.Unrecognized(qrCode), + ) + } + val matchingNetworkIds = matchingCoins.map { it.network.id }.toSet() val context = PaymentUriResolveHelper.ResolveContext( parsed = parsed, 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 61627b9738..bbf95cf345 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 @@ -8,6 +8,7 @@ import java.math.BigDecimal import java.math.MathContext internal class TronPaymentUriParser( + private val blockchainDataProvider: QrContentClassifierParser.BlockchainDataProvider, private val helper: PaymentUriResolveHelper = PaymentUriResolveHelper(), ) : PaymentUriParser { @@ -31,6 +32,15 @@ internal class TronPaymentUriParser( ) } + val isAddressValid = matchingCoins.any { + blockchainDataProvider.validateAddress(it.network, parsed.address) + } + if (!isAddressValid) { + return PaymentUriParser.ParseResult.RecognizedError( + ClassifiedQrContent.Error.Unrecognized(qrCode), + ) + } + val matchingNetworkIds = matchingCoins.map { it.network.id }.toSet() val context = PaymentUriResolveHelper.ResolveContext( parsed = parsed, 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 723ea0ef8c..4d3c46bdec 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 @@ -3,15 +3,21 @@ package com.tangem.data.qrscanning import com.google.common.truth.Truth.assertThat import com.tangem.data.qrscanning.parser.Bip321PaymentUriParser import com.tangem.data.qrscanning.parser.PaymentUriParser +import com.tangem.data.qrscanning.parser.QrContentClassifierParser import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.qrscanning.models.ClassifiedQrContent +import io.mockk.every +import io.mockk.mockk import org.junit.Test import java.math.BigDecimal internal class Bip321PaymentUriParserTest { - private val parser = Bip321PaymentUriParser() + private val blockchainDataProvider = mockk { + every { validateAddress(any(), any()) } returns true + } + private val parser = Bip321PaymentUriParser(blockchainDataProvider) // region Basic parsing @@ -138,6 +144,21 @@ internal class Bip321PaymentUriParserTest { // region Unsupported network + @Test + fun `invalid address returns Unrecognized error`() { + every { blockchainDataProvider.validateAddress(any(), eq("InvalidBtcAddress")) } returns false + + val result = parser.parse( + qrCode = "bitcoin:InvalidBtcAddress?amount=0.5", + coins = listOf(bitcoinCoin), + allCurrencies = listOf(bitcoinCoin), + ) + + assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedError::class.java) + val error = (result as PaymentUriParser.ParseResult.RecognizedError).error + assertThat(error).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java) + } + @Test fun `bitcoin URI with no matching coin returns UnsupportedNetwork`() { val result = parser.parse( 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 f43f5e513e..e76d3c6910 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 @@ -17,6 +17,7 @@ internal class Eip681PaymentUriParserTest { private val blockchainDataProvider = mockk { every { getChainId(any()) } returns null every { getBlockchainNameByChainId(any()) } returns null + every { validateAddress(any(), any()) } returns true } private val parser = Eip681PaymentUriParser(blockchainDataProvider) @@ -173,6 +174,24 @@ internal class Eip681PaymentUriParserTest { assertThat(error).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java) } + @Test + fun `ERC-20 transfer with invalid recipient address returns Unrecognized error`() { + every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L + every { blockchainDataProvider.validateAddress(any(), eq("0xInvalidRecipient")) } returns false + + val usdcToken = buildToken("ethereum", "USDC", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") + + val result = parser.parse( + qrCode = "ethereum:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48@1/transfer?address=0xInvalidRecipient&uint256=1000000", + coins = listOf(ethereumCoin), + allCurrencies = listOf(ethereumCoin, usdcToken), + ) + + assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedError::class.java) + val error = (result as PaymentUriParser.ParseResult.RecognizedError).error + assertThat(error).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java) + } + // endregion @Test 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 5f2935b741..22d0e53a40 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 @@ -2,16 +2,22 @@ package com.tangem.data.qrscanning import com.google.common.truth.Truth.assertThat import com.tangem.data.qrscanning.parser.PaymentUriParser +import com.tangem.data.qrscanning.parser.QrContentClassifierParser import com.tangem.data.qrscanning.parser.SolanaPaymentUriParser import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.qrscanning.models.ClassifiedQrContent +import io.mockk.every +import io.mockk.mockk import org.junit.Test import java.math.BigDecimal internal class SolanaPaymentUriParserTest { - private val parser = SolanaPaymentUriParser() + private val blockchainDataProvider = mockk { + every { validateAddress(any(), any()) } returns true + } + private val parser = SolanaPaymentUriParser(blockchainDataProvider) // region Scheme matching @@ -112,6 +118,21 @@ internal class SolanaPaymentUriParserTest { // region Unsupported network + @Test + fun `invalid address returns Unrecognized error`() { + every { blockchainDataProvider.validateAddress(any(), eq("InvalidSolAddress")) } returns false + + val result = parser.parse( + qrCode = "solana:InvalidSolAddress?amount=1", + coins = listOf(solanaCoin), + allCurrencies = listOf(solanaCoin), + ) + + assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedError::class.java) + val error = (result as PaymentUriParser.ParseResult.RecognizedError).error + assertThat(error).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java) + } + @Test fun `no matching solana coin returns UnsupportedNetwork`() { val result = parser.parse( 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 63529bdd52..2525689756 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 @@ -2,16 +2,22 @@ package com.tangem.data.qrscanning import com.google.common.truth.Truth.assertThat import com.tangem.data.qrscanning.parser.PaymentUriParser +import com.tangem.data.qrscanning.parser.QrContentClassifierParser import com.tangem.data.qrscanning.parser.TronPaymentUriParser import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.qrscanning.models.ClassifiedQrContent +import io.mockk.every +import io.mockk.mockk import org.junit.Test import java.math.BigDecimal internal class TronPaymentUriParserTest { - private val parser = TronPaymentUriParser() + private val blockchainDataProvider = mockk { + every { validateAddress(any(), any()) } returns true + } + private val parser = TronPaymentUriParser(blockchainDataProvider) // region Scheme matching @@ -152,6 +158,21 @@ internal class TronPaymentUriParserTest { // region Unsupported network + @Test + fun `invalid address returns Unrecognized error`() { + every { blockchainDataProvider.validateAddress(any(), eq("InvalidTronAddress")) } returns false + + val result = parser.parse( + qrCode = "tron:InvalidTronAddress?amount=1", + coins = listOf(tronCoin), + allCurrencies = listOf(tronCoin), + ) + + assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedError::class.java) + val error = (result as PaymentUriParser.ParseResult.RecognizedError).error + assertThat(error).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java) + } + @Test fun `no matching tron coin returns UnsupportedNetwork`() { val result = parser.parse(