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 3bc83d2a7c..3853849fcd 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 @@ -14,18 +14,22 @@ internal class Eip681PaymentUriParser( coins: List, allCurrencies: List, ): PaymentUriParser.ParseResult { - if (!qrCode.startsWith(SCHEME)) return PaymentUriParser.ParseResult.NotRecognized + if (!qrCode.startsWith(SCHEME)) { + return PaymentUriParser.ParseResult.NotRecognized + } val withoutScheme = qrCode.removePrefix(SCHEME) val parsed = parseEip681(withoutScheme) ?: return PaymentUriParser.ParseResult.NotRecognized val matchingCoins = findMatchingCoins(parsed.chainId, coins) - if (matchingCoins.isEmpty()) return PaymentUriParser.ParseResult.RecognizedButNoMatch + if (matchingCoins.isEmpty()) { + return PaymentUriParser.ParseResult.RecognizedButNoMatch + } val result = if (parsed.functionName == FUNCTION_TRANSFER) { resolveErc20Transfer(parsed, matchingCoins, allCurrencies) } else { - resolveNativeTransfer(parsed, matchingCoins) + resolveNativeTransfer(parsed, matchingCoins, allCurrencies) } return if (result != null) { PaymentUriParser.ParseResult.Success(result) @@ -37,6 +41,7 @@ internal class Eip681PaymentUriParser( private fun resolveNativeTransfer( parsed: Eip681Result, matchingCoins: List, + allCurrencies: List, ): ClassifiedQrContent.PaymentUri? { val valueWei = parsed.params[PARAM_VALUE]?.toBigDecimalOrNull() @@ -45,11 +50,20 @@ internal class Eip681PaymentUriParser( val decimals = matchingCoins.first().decimals val amount = valueWei?.fromSmallestUnit(decimals) + val matchingNetworkIds = matchingCoins.map { it.network.id }.toSet() + // If value is specified, this is a native coin transfer — return only coins + // If no value, it's just an address with scheme — return all currencies on the network + val matchingCurrencies = if (valueWei != null) { + matchingCoins + } else { + allCurrencies.filter { it.network.id in matchingNetworkIds } + } + return ClassifiedQrContent.PaymentUri( address = parsed.targetAddress, amount = amount, memo = null, - matchingCurrencies = matchingCoins, + matchingCurrencies = matchingCurrencies, ) } @@ -63,22 +77,22 @@ internal class Eip681PaymentUriParser( val matchingNetworkIds = matchingCoins.map { it.network.id }.toSet() - val tokens = allCurrencies.filterIsInstance() - - val token = tokens - .firstOrNull { token -> + val matchingTokens = allCurrencies.filterIsInstance() + .filter { token -> token.network.id in matchingNetworkIds && token.contractAddress.equals(contractAddress, ignoreCase = true) - } ?: return null + } + + if (matchingTokens.isEmpty()) return null val rawAmount = parsed.params[PARAM_UINT256]?.toBigDecimalOrNull() - val amount = rawAmount?.fromSmallestUnit(token.decimals) + val amount = rawAmount?.fromSmallestUnit(matchingTokens.first().decimals) return ClassifiedQrContent.PaymentUri( address = recipient, amount = amount, memo = null, - matchingCurrencies = listOf(token), + matchingCurrencies = matchingTokens, ) } diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt index 161619037d..6d970e3ec4 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/parser/QrContentClassifierParser.kt @@ -29,10 +29,9 @@ internal class QrContentClassifierParser( is PaymentUriParser.ParseResult.NotRecognized -> Unit } - val matchingNetworkIds = uniqueCoins + val matchingCoins = uniqueCoins .filter { coin -> blockchainDataProvider.validateAddress(coin.network, qrCode) } - .map { it.network.id } - .toSet() + val matchingNetworkIds = matchingCoins.map { it.network.id }.toSet() if (matchingNetworkIds.isNotEmpty()) { val matchingCurrencies = userCurrencies.filter { it.network.id in matchingNetworkIds } 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 b270ef58c1..f78e4d22da 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 @@ -70,7 +70,7 @@ internal class Eip681PaymentUriParserTest { } @Test - fun `native transfer includes only coins, not tokens`() { + fun `native transfer with value returns only coins`() { every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L val usdcToken = buildToken("ethereum", "USDC", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") @@ -85,6 +85,22 @@ internal class Eip681PaymentUriParserTest { assertThat(result!!.matchingCurrencies).containsExactly(ethereumCoin) } + @Test + fun `native transfer without value returns all currencies on matching network`() { + every { blockchainDataProvider.getChainId(ethereumCoin.network) } returns 1L + + val usdcToken = buildToken("ethereum", "USDC", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") + + val result = parser.parse( + qrCode = "ethereum:0xRecipient@1", + coins = listOf(ethereumCoin), + allCurrencies = listOf(ethereumCoin, usdcToken), + ).asSuccess() + + assertThat(result).isNotNull() + assertThat(result!!.matchingCurrencies).containsExactly(ethereumCoin, usdcToken) + } + // endregion // region ERC-20 transfer diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt index 2e5cea6b3a..bfa7169dac 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt @@ -27,19 +27,19 @@ class ResolveQrSendTargetsUseCase( val currencyLocations = mutableMapOf>() val totalPerAccount = mutableMapOf() - for (accountList in allAccountLists) { - for (account in accountList.accounts.filterIsInstance()) { - val location = CurrencyLocation( - walletName = walletNamesMap[account.accountId.userWalletId] - ?: account.accountId.userWalletId.stringValue, - accountId = account.accountId, - accountName = account.accountName, - ) - totalPerAccount[account.accountId] = account.cryptoCurrencies.size - for (currency in account.cryptoCurrencies) { - allCurrencies.add(currency) - currencyLocations.getOrPut(currency.id) { mutableListOf() }.add(location) - } + val allAccounts = allAccountLists.flatMap { it.accounts }.filterIsInstance() + + for (account in allAccounts) { + val location = CurrencyLocation( + walletName = walletNamesMap[account.accountId.userWalletId] + ?: account.accountId.userWalletId.stringValue, + accountId = account.accountId, + accountName = account.accountName, + ) + totalPerAccount[account.accountId] = account.cryptoCurrencies.size + for (currency in account.cryptoCurrencies) { + allCurrencies.add(currency) + currencyLocations.getOrPut(currency.id) { mutableListOf() }.add(location) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 840e428d26..8134a10127 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -30,10 +30,12 @@ import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2 import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams +import com.tangem.features.send.v2.api.NetworkSelectionComponent import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent import dagger.assisted.Assisted @@ -52,6 +54,7 @@ internal class WalletComponent @AssistedInject constructor( private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, + private val networkSelectionComponentFactory: NetworkSelectionComponent.Factory, private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { @@ -156,6 +159,41 @@ internal class WalletComponent @AssistedInject constructor( ), ) } + is WalletDialogConfig.NetworkSelection -> { + networkSelectionComponentFactory.create( + context = childByContext(componentContext), + params = NetworkSelectionComponent.Params( + address = dialogConfig.address, + amount = dialogConfig.amount, + memo = dialogConfig.memo, + walletGroups = dialogConfig.walletGroups.map { walletGroup -> + NetworkSelectionComponent.Params.WalletGroup( + userWalletId = walletGroup.userWalletId, + walletName = walletGroup.walletName, + accounts = walletGroup.accounts.map { accountGroup -> + NetworkSelectionComponent.Params.AccountGroup( + accountId = accountGroup.accountId, + accountName = accountGroup.accountName, + currencies = accountGroup.currencies, + hiddenTokensCount = accountGroup.hiddenTokensCount, + ) + }, + ) + }, + onTokenSelected = { userWalletId, currency -> + model.innerWalletRouter.dialogNavigation.dismiss() + model.innerWalletRouter.openSend( + userWalletId = userWalletId, + currency = currency, + address = dialogConfig.address, + amount = dialogConfig.amount?.parseBigDecimal(currency.decimals), + tag = dialogConfig.memo, + ) + }, + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + ), + ) + } } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index dd76e89962..44c6eb55fc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -776,7 +776,7 @@ internal class WalletModel @Inject constructor( ) } is QrSendTarget.Multiple -> { - // TODO: [REDACTED_TASK_KEY] Bottom sheet: Wallets (dropdown) → Accounts → Tokens + innerWalletRouter.openNetworkSelectionBottomSheet(target) } is QrSendTarget.Unknown -> { // TODO: [REDACTED_TASK_KEY] Error handling for unsupported and invalid QR codes diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index f123c9c3c1..b0da023377 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -13,6 +13,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.qrscanning.models.QrSendTarget import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig @@ -197,6 +198,30 @@ internal class DefaultWalletRouter @Inject constructor( ) } + override fun openNetworkSelectionBottomSheet(target: QrSendTarget.Multiple) { + dialogNavigation.activate( + configuration = WalletDialogConfig.NetworkSelection( + address = target.address, + amount = target.amount, + memo = target.memo, + walletGroups = target.walletGroups.map { walletGroup -> + WalletDialogConfig.NetworkSelection.WalletGroupData( + userWalletId = walletGroup.userWalletId, + walletName = walletGroup.walletName, + accounts = walletGroup.accounts.map { accountGroup -> + WalletDialogConfig.NetworkSelection.AccountGroupData( + accountId = accountGroup.accountId, + accountName = accountGroup.accountName, + currencies = accountGroup.currencies, + hiddenTokensCount = accountGroup.hiddenTokensCount, + ) + }, + ) + }, + ), + ) + } + inner class OrganizeCallbacks : OrganizeTokensComponent.Callback { override fun onDismiss() { dialogNavigation.dismiss() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index d87153341b..52741298a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -8,6 +8,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.qrscanning.models.QrSendTarget import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig @@ -95,4 +96,7 @@ internal interface InnerWalletRouter { /** Open send screen with prefilled destination */ fun openSend(userWalletId: UserWalletId, currency: CryptoCurrency, address: String, amount: String?, tag: String?) + + /** Open network selection bottom sheet for multiple QR matches */ + fun openNetworkSelectionBottomSheet(target: QrSendTarget.Multiple) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index 01e89e2b5e..a8844378c1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -1,11 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.serialization.BigDecimalSerializer import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.details.TokenAction import kotlinx.collections.immutable.ImmutableList import kotlinx.serialization.Serializable +import java.math.BigDecimal /** * Wallet dialog config. Used to show Decompose dialogs @@ -44,4 +48,28 @@ internal sealed interface WalletDialogConfig { @Serializable data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig + + @Serializable + data class NetworkSelection( + val address: String, + val amount: @Serializable(BigDecimalSerializer::class) BigDecimal?, + val memo: String?, + val walletGroups: List, + ) : WalletDialogConfig { + + @Serializable + data class WalletGroupData( + val userWalletId: UserWalletId, + val walletName: String, + val accounts: List, + ) + + @Serializable + data class AccountGroupData( + val accountId: AccountId, + val accountName: AccountName, + val currencies: List, + val hiddenTokensCount: Int = 0, + ) + } } \ No newline at end of file