Updated on 2026-08-14
This commit is contained in:
parent
0ce75f1445
commit
e0c101f15f
12 changed files with 115 additions and 32 deletions
|
|
@ -17,11 +17,17 @@ internal class Bip321PaymentUriParser(
|
|||
val (matchingCoins, withoutScheme) = schemeAndRest
|
||||
|
||||
val parsed = QrSentUriParser().parse(withoutScheme)
|
||||
?: return PaymentUriParser.ParseResult.RecognizedButNoMatch
|
||||
?: return PaymentUriParser.ParseResult.RecognizedError(
|
||||
ClassifiedQrContent.Error.Unrecognized(qrCode),
|
||||
)
|
||||
|
||||
val matchingNetworkIds = matchingCoins.map { it.network.id }.toSet()
|
||||
val matchingCurrencies = allCurrencies.filter { it.network.id in matchingNetworkIds }
|
||||
if (matchingCurrencies.isEmpty()) return PaymentUriParser.ParseResult.RecognizedButNoMatch
|
||||
if (matchingCurrencies.isEmpty()) {
|
||||
return PaymentUriParser.ParseResult.RecognizedError(
|
||||
ClassifiedQrContent.Error.UnsupportedNetwork,
|
||||
)
|
||||
}
|
||||
|
||||
return PaymentUriParser.ParseResult.Success(
|
||||
ClassifiedQrContent.PaymentUri(
|
||||
|
|
|
|||
|
|
@ -23,10 +23,17 @@ internal class Eip681PaymentUriParser(
|
|||
|
||||
val matchingCoins = findMatchingCoins(parsed.chainId, coins)
|
||||
if (matchingCoins.isEmpty()) {
|
||||
return PaymentUriParser.ParseResult.RecognizedButNoMatch
|
||||
return PaymentUriParser.ParseResult.RecognizedError(
|
||||
ClassifiedQrContent.Error.UnsupportedNetwork,
|
||||
)
|
||||
}
|
||||
|
||||
val result = if (parsed.functionName == FUNCTION_TRANSFER) {
|
||||
if (PARAM_ADDRESS !in parsed.params) {
|
||||
return PaymentUriParser.ParseResult.RecognizedError(
|
||||
ClassifiedQrContent.Error.Unrecognized(qrCode),
|
||||
)
|
||||
}
|
||||
resolveErc20Transfer(parsed, matchingCoins, allCurrencies)
|
||||
} else {
|
||||
resolveNativeTransfer(parsed, matchingCoins, allCurrencies)
|
||||
|
|
@ -34,7 +41,9 @@ internal class Eip681PaymentUriParser(
|
|||
return if (result != null) {
|
||||
PaymentUriParser.ParseResult.Success(result)
|
||||
} else {
|
||||
PaymentUriParser.ParseResult.RecognizedButNoMatch
|
||||
PaymentUriParser.ParseResult.RecognizedError(
|
||||
ClassifiedQrContent.Error.UnsupportedNetwork,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ internal interface PaymentUriParser {
|
|||
/** URI format not recognized by this parser. */
|
||||
data object NotRecognized : ParseResult()
|
||||
|
||||
/** URI format recognized but no matching currencies found. */
|
||||
data object RecognizedButNoMatch : ParseResult()
|
||||
/** URI format recognized but resulted in an error. */
|
||||
data class RecognizedError(val error: ClassifiedQrContent.Error) : ParseResult()
|
||||
|
||||
/** Successfully parsed with matching currencies. */
|
||||
data class Success(val content: ClassifiedQrContent.PaymentUri) : ParseResult()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.qrscanning.parser
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -25,7 +26,7 @@ internal class QrContentClassifierParser(
|
|||
|
||||
when (val paymentUriResult = tryParsePaymentUri(qrCode, uniqueCoins, userCurrencies)) {
|
||||
is PaymentUriParser.ParseResult.Success -> return paymentUriResult.content
|
||||
is PaymentUriParser.ParseResult.RecognizedButNoMatch -> return ClassifiedQrContent.Unknown(qrCode)
|
||||
is PaymentUriParser.ParseResult.RecognizedError -> return paymentUriResult.error
|
||||
is PaymentUriParser.ParseResult.NotRecognized -> Unit
|
||||
}
|
||||
|
||||
|
|
@ -35,13 +36,18 @@ internal class QrContentClassifierParser(
|
|||
|
||||
if (matchingNetworkIds.isNotEmpty()) {
|
||||
val matchingCurrencies = userCurrencies.filter { it.network.id in matchingNetworkIds }
|
||||
|
||||
return ClassifiedQrContent.PlainAddress(
|
||||
address = qrCode,
|
||||
matchingCurrencies = matchingCurrencies,
|
||||
)
|
||||
}
|
||||
|
||||
return ClassifiedQrContent.Unknown(qrCode)
|
||||
if (blockchainDataProvider.isSupportedAddress(qrCode)) {
|
||||
return ClassifiedQrContent.Error.UnsupportedNetwork
|
||||
}
|
||||
|
||||
return ClassifiedQrContent.Error.Unrecognized(qrCode)
|
||||
}
|
||||
|
||||
private fun tryParsePaymentUri(
|
||||
|
|
@ -70,6 +76,7 @@ internal class QrContentClassifierParser(
|
|||
fun getShareSchemes(network: Network): List<String>
|
||||
fun validateAddress(network: Network, address: String): Boolean
|
||||
fun getChainId(network: Network): Long?
|
||||
fun isSupportedAddress(address: String): Boolean
|
||||
}
|
||||
|
||||
internal class DefaultBlockchainDataProvider : BlockchainDataProvider {
|
||||
|
|
@ -84,6 +91,14 @@ internal class QrContentClassifierParser(
|
|||
override fun getChainId(network: Network): Long? {
|
||||
return runCatching { network.toBlockchain().getChainId()?.toLong() }.getOrNull()
|
||||
}
|
||||
|
||||
override fun isSupportedAddress(address: String): Boolean {
|
||||
return Blockchain.entries
|
||||
.filter { !it.isTestnet() }
|
||||
.any { blockchain ->
|
||||
runCatching { blockchain.validateAddress(address) }.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ internal class Eip681PaymentUriParserTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `ERC-20 transfer with unknown token returns RecognizedButNoMatch`() {
|
||||
fun `ERC-20 transfer with unknown token returns UnsupportedNetwork error`() {
|
||||
every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L
|
||||
|
||||
val result = parser.parse(
|
||||
|
|
@ -151,11 +151,13 @@ internal class Eip681PaymentUriParserTest {
|
|||
allCurrencies = listOf(ethereumCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedButNoMatch::class.java)
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedError::class.java)
|
||||
val error = (result as PaymentUriParser.ParseResult.RecognizedError).error
|
||||
assertThat(error).isInstanceOf(ClassifiedQrContent.Error.UnsupportedNetwork::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ERC-20 transfer without address param returns RecognizedButNoMatch`() {
|
||||
fun `ERC-20 transfer without address param returns Unrecognized error`() {
|
||||
every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L
|
||||
|
||||
val usdcToken = buildToken("ethereum", "USDC", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
|
||||
|
|
@ -166,7 +168,9 @@ internal class Eip681PaymentUriParserTest {
|
|||
allCurrencies = listOf(ethereumCoin, usdcToken),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedButNoMatch::class.java)
|
||||
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
|
||||
|
|
@ -192,7 +196,7 @@ internal class Eip681PaymentUriParserTest {
|
|||
// region Chain ID matching
|
||||
|
||||
@Test
|
||||
fun `chain_id mismatch returns RecognizedButNoMatch`() {
|
||||
fun `chain_id mismatch returns UnsupportedNetwork error`() {
|
||||
every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L
|
||||
|
||||
val result = parser.parse(
|
||||
|
|
@ -201,7 +205,9 @@ internal class Eip681PaymentUriParserTest {
|
|||
allCurrencies = listOf(ethereumCoin),
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedButNoMatch::class.java)
|
||||
assertThat(result).isInstanceOf(PaymentUriParser.ParseResult.RecognizedError::class.java)
|
||||
val error = (result as PaymentUriParser.ParseResult.RecognizedError).error
|
||||
assertThat(error).isInstanceOf(ClassifiedQrContent.Error.UnsupportedNetwork::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ internal class QrContentClassifierTest {
|
|||
every { getShareSchemes(any()) } returns emptyList()
|
||||
every { validateAddress(any(), any()) } returns false
|
||||
every { getChainId(any()) } returns null
|
||||
every { isSupportedAddress(any()) } returns false
|
||||
}
|
||||
private val paymentUriParser = mockk<PaymentUriParser> {
|
||||
every { parse(any(), any(), any()) } returns PaymentUriParser.ParseResult.NotRecognized
|
||||
|
|
@ -63,7 +64,7 @@ internal class QrContentClassifierTest {
|
|||
|
||||
val result = classifier.parse(url, listOf(bitcoinCoin))
|
||||
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Unknown::class.java)
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -72,7 +73,7 @@ internal class QrContentClassifierTest {
|
|||
|
||||
val result = classifier.parse(url, listOf(bitcoinCoin))
|
||||
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Unknown::class.java)
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -162,22 +163,22 @@ internal class QrContentClassifierTest {
|
|||
fun `Random string returns Unknown`() {
|
||||
val result = classifier.parse("hello world", listOf(bitcoinCoin, ethereumCoin))
|
||||
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Unknown::class.java)
|
||||
assertThat((result as ClassifiedQrContent.Unknown).raw).isEqualTo("hello world")
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java)
|
||||
assertThat((result as ClassifiedQrContent.Error.Unrecognized).raw).isEqualTo("hello world")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Empty string returns Unknown`() {
|
||||
val result = classifier.parse("", listOf(bitcoinCoin))
|
||||
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Unknown::class.java)
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Empty currencies list returns Unknown`() {
|
||||
val result = classifier.parse("0x1234567890abcdef1234567890abcdef12345678", emptyList())
|
||||
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Unknown::class.java)
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -186,7 +187,7 @@ internal class QrContentClassifierTest {
|
|||
|
||||
val result = classifier.parse("0x1234", listOf(token))
|
||||
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Unknown::class.java)
|
||||
assertThat(result).isInstanceOf(ClassifiedQrContent.Error.Unrecognized::class.java)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue