Updated on 2026-08-14
This commit is contained in:
parent
5e7e57b942
commit
1ec1196e12
29 changed files with 466 additions and 50 deletions
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue