Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 15:06:56 +07:00
commit dcda504df6
40 changed files with 1442 additions and 182 deletions

View file

@ -284,7 +284,7 @@ class GetAccountCurrencyStatusUseCaseTest {
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
.let(::getEmittedValues)
// Assert

View file

@ -30,6 +30,11 @@ interface NetworksRepository {
*/
suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network.RawID): List<CryptoCurrencyAddress>
/**
* Returns the default address for the given [network] in the selected [userWalletId]
*/
suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String?
/** Checks if there are cached statuses for given [userWalletId] */
suspend fun hasCachedStatuses(userWalletId: UserWalletId): Boolean
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.promo.models.StoryContent
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.seconds
@ -39,12 +40,14 @@ class GetStoryContentUseCase(
}
}
@OptIn(FlowPreview::class)
private fun isFCAAllowed(id: String): Flow<Boolean> {
return if (id == StoryContentIds.STORY_FIRST_TIME_SWAP.id) {
settingsRepository.getUserCountryCode()
.filterNotNull()
.timeout(5.seconds)
.timeout(3.seconds)
.map { !it.needApplyFCARestrictions() }
.catch { emit(true) }
} else {
flowOf(true)
}

View file

@ -14,6 +14,7 @@ dependencies {
api(projects.domain.models)
implementation(projects.domain.account)
implementation(projects.domain.common)
implementation(projects.domain.networks)
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.tokens.models)

View file

@ -19,6 +19,11 @@ sealed interface ClassifiedQrContent {
val matchingCurrencies: List<CryptoCurrency>,
) : ClassifiedQrContent
data class PaymentUriWarning(
val paymentUri: PaymentUri,
val unsupportedParams: Map<String, String>,
) : ClassifiedQrContent
sealed interface Error : ClassifiedQrContent {
/** QR code not recognized by any parser */

View file

@ -39,6 +39,13 @@ sealed class QrSendTarget {
)
}
data object AddressSameAsWallet : QrSendTarget()
data class Warning(
val target: QrSendTarget,
val unsupportedParams: Map<String, String>,
) : QrSendTarget()
data class WalletConnect(val uri: String) : QrSendTarget()
data class Error(val error: ClassifiedQrContent.Error) : QrSendTarget()

View file

@ -5,17 +5,23 @@ import com.tangem.domain.models.account.Account
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.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import com.tangem.domain.qrscanning.models.QrSendTarget
import kotlinx.coroutines.awaitAll
class ResolveQrSendTargetsUseCase(
private val multiAccountListSupplier: MultiAccountListSupplier,
private val qrScanningEventsRepository: QrScanningEventsRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val networksRepository: NetworksRepository,
) {
suspend operator fun invoke(qrCode: String): QrSendTarget {
@ -49,7 +55,7 @@ class ResolveQrSendTargetsUseCase(
return resolve(classified, portfolioIndex)
}
private fun resolve(classified: ClassifiedQrContent, portfolioIndex: PortfolioIndex): QrSendTarget {
private suspend fun resolve(classified: ClassifiedQrContent, portfolioIndex: PortfolioIndex): QrSendTarget {
return when (classified) {
is ClassifiedQrContent.WalletConnect -> QrSendTarget.WalletConnect(classified.uri)
is ClassifiedQrContent.Error -> QrSendTarget.Error(classified)
@ -67,17 +73,29 @@ class ResolveQrSendTargetsUseCase(
matchingCurrencies = classified.matchingCurrencies,
portfolioIndex = portfolioIndex,
)
is ClassifiedQrContent.PaymentUriWarning -> {
val inner = resolve(classified.paymentUri, portfolioIndex)
QrSendTarget.Warning(
target = inner,
unsupportedParams = classified.unsupportedParams,
)
}
}
}
private fun resolveAddressTarget(
private suspend fun resolveAddressTarget(
address: String,
amount: BigDecimal?,
memo: String?,
matchingCurrencies: List<CryptoCurrency>,
portfolioIndex: PortfolioIndex,
): QrSendTarget {
val walletGroups = buildWalletGroups(matchingCurrencies, portfolioIndex)
val ownAddressNetworks = findOwnAddressNetworks(address, matchingCurrencies, portfolioIndex)
val walletGroups = buildWalletGroups(matchingCurrencies, portfolioIndex, ownAddressNetworks)
if (walletGroups.isEmpty()) {
return QrSendTarget.AddressSameAsWallet
}
val singleGroup = walletGroups.singleOrNull()
val singleCurrency = singleGroup?.accounts?.singleOrNull()?.currencies?.singleOrNull()
@ -100,9 +118,33 @@ class ResolveQrSendTargetsUseCase(
}
}
private suspend fun findOwnAddressNetworks(
address: String,
matchingCurrencies: List<CryptoCurrency>,
portfolioIndex: PortfolioIndex,
): Map<UserWalletId, List<Network.ID>> = coroutineScope {
matchingCurrencies.distinctBy { it.id }
.flatMap { currency ->
portfolioIndex.currencyLocations[currency.id].orEmpty().map {
it.accountId.userWalletId to currency.network
}
}
.distinct()
.map { (walletId, network) ->
async {
val ownAddress = networksRepository.getDefaultAddress(walletId, network)
if (ownAddress == address) walletId to network else null
}
}
.awaitAll()
.filterNotNull()
.groupBy(keySelector = { it.first }, valueTransform = { it.second.id })
}
private fun buildWalletGroups(
matchingCurrencies: List<CryptoCurrency>,
portfolioIndex: PortfolioIndex,
ownAddressNetworks: Map<UserWalletId, List<Network.ID>>,
): List<QrSendTarget.Multiple.WalletGroup> {
val walletMap = linkedMapOf<UserWalletId, WalletInfo>()
val uniqueCurrencies = matchingCurrencies.distinctBy { it.id }
@ -110,13 +152,18 @@ class ResolveQrSendTargetsUseCase(
for (currency in uniqueCurrencies) {
val locations = portfolioIndex.currencyLocations[currency.id].orEmpty()
for (location in locations) {
val walletInfo = walletMap.getOrPut(location.accountId.userWalletId) {
WalletInfo(location.walletName, linkedMapOf())
val walletId = location.accountId.userWalletId
val isOwnAddress = ownAddressNetworks[walletId]?.contains(currency.network.id) == true
if (!isOwnAddress) {
val walletInfo = walletMap.getOrPut(walletId) {
WalletInfo(location.walletName, linkedMapOf())
}
val accountInfo = walletInfo.accounts.getOrPut(location.accountId) {
AccountInfo(location.accountName, mutableListOf())
}
accountInfo.currencies.add(currency)
}
val accountInfo = walletInfo.accounts.getOrPut(location.accountId) {
AccountInfo(location.accountName, mutableListOf())
}
accountInfo.currencies.add(currency)
}
}