Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-27 06:30:43 +03:00
parent 5e7e57b942
commit 1ec1196e12
29 changed files with 466 additions and 50 deletions

View file

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

View file

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

View file

@ -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()
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
)
}
/**

View file

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

View file

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

View file

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

View file

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

View file

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