Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-25 17:14:25 +03:00
parent 76d77bd83c
commit c44508b705
9 changed files with 137 additions and 7 deletions

View file

@ -26,9 +26,9 @@ internal object QrScanningDataModule {
blockchainDataProvider = blockchainDataProvider,
paymentUriParsers = setOf(
Eip681PaymentUriParser(blockchainDataProvider),
TronPaymentUriParser(),
SolanaPaymentUriParser(),
Bip321PaymentUriParser(),
TronPaymentUriParser(blockchainDataProvider),
SolanaPaymentUriParser(blockchainDataProvider),
Bip321PaymentUriParser(blockchainDataProvider),
),
),
)

View file

@ -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,

View file

@ -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),
)

View file

@ -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,

View file

@ -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,

View file

@ -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<QrContentClassifierParser.BlockchainDataProvider> {
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(

View file

@ -17,6 +17,7 @@ internal class Eip681PaymentUriParserTest {
private val blockchainDataProvider = mockk<QrContentClassifierParser.BlockchainDataProvider> {
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

View file

@ -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<QrContentClassifierParser.BlockchainDataProvider> {
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(

View file

@ -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<QrContentClassifierParser.BlockchainDataProvider> {
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(