diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 700cd6bdfc..b719015a7a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -5,6 +5,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.promo.PromoRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.operations.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.swap.SwapFeatureToggles @@ -62,17 +63,11 @@ internal object TokensDomainModule { @Singleton fun provideGetTokenListUseCase( currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, - tokensFeatureToggles: TokensFeatureToggles, + baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, ): GetTokenListUseCase { return GetTokenListUseCase( currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - tokensFeatureToggles = tokensFeatureToggles, + currenciesStatusesOperations = baseCurrenciesStatusesOperations, ) } @@ -88,18 +83,12 @@ internal object TokensDomainModule { @Provides @Singleton fun provideGetCurrencyUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, + baseCurrencyStatusOperations: BaseCurrencyStatusOperations, dispatchers: CoroutineDispatcherProvider, ): GetCurrencyStatusUpdatesUseCase { return GetCurrencyStatusUpdatesUseCase( - currenciesRepository, - quotesRepository, - networksRepository, - stakingRepository, - dispatchers, + currencyStatusOperations = baseCurrencyStatusOperations, + dispatchers = dispatchers, ) } @@ -107,16 +96,12 @@ internal object TokensDomainModule { @Singleton fun provideGetAllWalletsCryptoCurrencyStatusesUseCase( currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, + currencyStatusOperations: BaseCurrencyStatusOperations, dispatchers: CoroutineDispatcherProvider, ): GetAllWalletsCryptoCurrencyStatusesUseCase { return GetAllWalletsCryptoCurrencyStatusesUseCase( currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, + currencyStatusOperations = currencyStatusOperations, dispatchers = dispatchers, ) } @@ -126,38 +111,30 @@ internal object TokensDomainModule { fun provideGetCurrencyWarningsUseCase( walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, networksRepository: NetworksRepository, currencyChecksRepository: CurrencyChecksRepository, - stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, + baseCurrencyStatusOperations: BaseCurrencyStatusOperations, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, networksRepository = networksRepository, currencyChecksRepository = currencyChecksRepository, - stakingRepository = stakingRepository, dispatchers = dispatchers, + currencyStatusOperations = baseCurrencyStatusOperations, ) } @Provides @Singleton fun provideGetPrimaryCurrencyUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, + currencyStatusOperations: BaseCurrencyStatusOperations, dispatchers: CoroutineDispatcherProvider, ): GetPrimaryCurrencyStatusUpdatesUseCase { return GetPrimaryCurrencyStatusUpdatesUseCase( - currenciesRepository, - quotesRepository, - networksRepository, - stakingRepository, - dispatchers, + currencyStatusOperations = currencyStatusOperations, + dispatchers = dispatchers, ) } @@ -186,19 +163,9 @@ internal object TokensDomainModule { @Provides @Singleton fun providesGetCryptoCurrencyStatusSyncUseCase( - currenciesRepository: CurrenciesRepository, - dispatcherProvider: CoroutineDispatcherProvider, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, + currencyStatusOperations: BaseCurrencyStatusOperations, ): GetCryptoCurrencyStatusSyncUseCase { - return GetCryptoCurrencyStatusSyncUseCase( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - dispatchers = dispatcherProvider, - ) + return GetCryptoCurrencyStatusSyncUseCase(currencyStatusOperations) } @Provides @@ -236,40 +203,32 @@ internal object TokensDomainModule { rampStateManager: RampStateManager, walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, promoRepository: PromoRepository, swapFeatureToggles: SwapFeatureToggles, dispatchers: CoroutineDispatcherProvider, + currencyStatusOperations: BaseCurrencyStatusOperations, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( rampManager = rampStateManager, walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, stakingRepository = stakingRepository, promoRepository = promoRepository, swapFeatureToggles = swapFeatureToggles, dispatchers = dispatchers, + currencyStatusOperations = currencyStatusOperations, ) } @Provides @Singleton fun provideGetCurrencyStatusByNetworkUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, + currencyStatusOperations: BaseCurrencyStatusOperations, dispatchers: CoroutineDispatcherProvider, ): GetNetworkCoinStatusUseCase { return GetNetworkCoinStatusUseCase( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, + currencyStatusOperations = currencyStatusOperations, dispatchers = dispatchers, ) } @@ -278,17 +237,11 @@ internal object TokensDomainModule { @Singleton fun provideGetFeePaidCryptoCurrencyStatusSyncUseCase( currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, - dispatchers: CoroutineDispatcherProvider, + currencyStatusOperations: BaseCurrencyStatusOperations, ): GetFeePaidCryptoCurrencyStatusSyncUseCase { return GetFeePaidCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - dispatchers = dispatchers, + currencyStatusOperations = currencyStatusOperations, ) } @@ -359,17 +312,9 @@ internal object TokensDomainModule { @Provides @Singleton fun provideGetWalletTotalBalanceUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, + baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, ): GetWalletTotalBalanceUseCase { - return GetWalletTotalBalanceUseCase( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) + return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations) } @Provides @@ -392,4 +337,56 @@ internal object TokensDomainModule { ): GetCurrencyCheckUseCase { return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers) } + + @Provides + @Singleton + fun provideBaseCurrenciesStatusesOperations( + tokensFeatureToggles: TokensFeatureToggles, + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + stakingRepository: StakingRepository, + ): BaseCurrenciesStatusesOperations { + return if (tokensFeatureToggles.isBalancesCachingEnabled) { + CachedCurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + } else { + LceCurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + } + } + + @Provides + @Singleton + fun provideBaseCurrencyStatusOperations( + tokensFeatureToggles: TokensFeatureToggles, + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + stakingRepository: StakingRepository, + ): BaseCurrencyStatusOperations { + return if (tokensFeatureToggles.isBalancesCachingEnabled) { + CachedCurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + } else { + CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + ) + } + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt index d33e9330f1..f5d2e66df6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt @@ -14,28 +14,8 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), ) } - is ScenarioUnavailabilityReason.PendingTransaction -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_send, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_pending_transaction_sell, - formatArgs = wrappedList(unavailabilityReason.networkName), - ) - } - } - is ScenarioUnavailabilityReason.EmptyBalance -> { - when (unavailabilityReason.withdrawalScenario) { - ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_send, - ) - ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( - id = R.string.token_button_unavailability_reason_empty_balance_sell, - ) - } - } + is ScenarioUnavailabilityReason.PendingTransaction -> unavailabilityReason.getDescription() + is ScenarioUnavailabilityReason.EmptyBalance -> unavailabilityReason.getDescription() is ScenarioUnavailabilityReason.BuyUnavailable -> { resourceReference( id = R.string.token_button_unavailability_reason_buy_unavailable, @@ -62,8 +42,38 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( id = R.string.warning_receive_blocked_hedera_token_association_required_message, ) + ScenarioUnavailabilityReason.UsedOutdatedData -> { + resourceReference(id = R.string.token_button_unavailability_reason_out_od_date_balance) + } ScenarioUnavailabilityReason.None -> { throw IllegalArgumentException("The unavailability reason must be other than None") } } +} + +private fun ScenarioUnavailabilityReason.PendingTransaction.getDescription(): TextReference { + return resourceReference( + id = when (withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> { + R.string.token_button_unavailability_reason_pending_transaction_send + } + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> { + R.string.token_button_unavailability_reason_pending_transaction_sell + } + }, + formatArgs = wrappedList(networkName), + ) +} + +private fun ScenarioUnavailabilityReason.EmptyBalance.getDescription(): TextReference { + return resourceReference( + id = when (withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> { + R.string.token_button_unavailability_reason_empty_balance_send + } + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> { + R.string.token_button_unavailability_reason_empty_balance_sell + } + }, + ) } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index c2100bf9dd..d209e29338 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -274,7 +274,7 @@ class TokenItemStateConverter( return PriceChangeConverter.fromBigDecimal(value = this) } - private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = getStatusSource() == StatusSource.CACHE + fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = getStatusSource() == StatusSource.CACHE private fun CryptoCurrencyStatus.Value.getStatusSource(): StatusSource? { return when (this) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index e5d163c5ab..b31620f4a4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt @@ -46,14 +46,6 @@ internal class DefaultNetworksStatusesStore( if (cachedStatuses.isNotEmpty()) { send(cachedStatuses) - - /** - * Required for storing cache data. - * This will help to recognize networks that are still uploaded. - * - * @see mergeStatuses - */ - storeAll(key = key, values = cachedStatuses) } runtimeDataStore.get(provideStringKey(key)) @@ -70,7 +62,28 @@ internal class DefaultNetworksStatusesStore( } override suspend fun getSyncOrNull(key: UserWalletId): Set? { - return runtimeDataStore.getSyncOrNull(key = provideStringKey(key)) + val runtimeStatuses = runtimeDataStore.getSyncOrNull(key = provideStringKey(key)) ?: return null + + val networks = runtimeStatuses.map(NetworkStatus::network).toSet() + + val cachedStatuses = persistenceDataStore.data.firstOrNull() + ?.get(key.stringValue) + ?.mapNotNullTo(mutableSetOf()) { cached -> + val network = networks.firstOrNull { + it.id == cached.networkId && + it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath) + } + ?: return@mapNotNullTo null + + NetworkStatusConverter(network = network, isCached = true).convert(value = cached) + } + .orEmpty() + + return mergeStatuses( + networks = networks, + cachedStatuses = cachedStatuses, + runtimeStatuses = runtimeStatuses, + ) } override suspend fun store(key: UserWalletId, value: NetworkStatus) { @@ -118,8 +131,18 @@ internal class DefaultNetworksStatusesStore( return networks.mapNotNullTo(hashSetOf()) { network -> val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network } - if (runtimeStatus == null || runtimeStatus.value is NetworkStatus.Unreachable) { - getCachedStatusIfPossible(cachedStatuses = cachedStatuses, network = network) + if (runtimeStatus == null) { + getCachedStatusIfPossible( + cachedStatuses = cachedStatuses, + network = network, + source = StatusSource.CACHE, + ) + } else if (runtimeStatus.value is NetworkStatus.Unreachable) { + getCachedStatusIfPossible( + cachedStatuses = cachedStatuses, + network = network, + source = StatusSource.ONLY_CACHE, + ) ?: runtimeStatus } else { runtimeStatus @@ -127,12 +150,16 @@ internal class DefaultNetworksStatusesStore( } } - private fun getCachedStatusIfPossible(cachedStatuses: Set, network: Network): NetworkStatus? { + private fun getCachedStatusIfPossible( + cachedStatuses: Set, + network: Network, + source: StatusSource, + ): NetworkStatus? { val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null val updatedCachedStatus = when (val status = cached.value) { - is NetworkStatus.NoAccount -> status.copy(source = StatusSource.ONLY_CACHE) - is NetworkStatus.Verified -> status.copy(source = StatusSource.ONLY_CACHE) + is NetworkStatus.NoAccount -> status.copy(source = source) + is NetworkStatus.Verified -> status.copy(source = source) is NetworkStatus.Unreachable, is NetworkStatus.MissedDerivation, -> null diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index 8cc69f2fda..09d14bde66 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -7,6 +7,7 @@ import com.tangem.datasource.local.quote.converter.QuoteConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote +import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -110,7 +111,7 @@ internal class DefaultQuotesStore( private suspend fun storeInRuntimeStore(values: Set) { runtimeStore.update(default = emptySet()) { saved -> - (saved + values).distinctBy { it.rawCurrencyId }.toSet() + saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } } } diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 277b47bbf2..ecef79cb21 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -674,6 +674,8 @@ Scanne die Karte oder Ring, um ihre Einstellungen zu ändern. Die Änderungen wirken sich nur auf die von dir gescannte Karte oder Ring aus und haben keine Auswirkungen auf andere mit deiner Wallet verknüpften Geräte. Halte deine Karte oder Ring bereit! Sicherheitswarnung + Nein, habe ich nicht + Ja, leite mich Dein Konto verfügt nicht über ausreichend Guthaben, um Kryptowährungen zu verkaufen. Bitte zahle den gewünschten Vermögenswert ein, um fortzufahren. Unzureichendes Guthaben Der Verkauf von Kryptowährung ist in Deiner Region derzeit nicht möglich. Wir arbeiten aktiv daran, Ihnen diese Option bald anzubieten – bleib dran! @@ -881,16 +883,16 @@ Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! - Du kannst Dich darauf verlassen, dass Deine Transaktionen jederzeit reibungslos ablaufen, da Sie ständig geprüft werden - Hilfe rund um die Uhr - Mit vertrauenswürdigen Börsenanbietern kannst Du mühelos Vermögenswerte tauschen, wobei alles sicher in Deiner Wallet verbleibt - Tausche sich mit uns - Höchste Sicherheit und geprüfte Anbieter gewährleisten, dass Deine Vermögenswerte bei jedem Swap geschützt sind - Sicherer denn je + Vertraue auf den rund um die Uhr verfügbaren Support bei allen Problemen + Immer für Dich da + Mehrere vertrauenswürdige Anbieter an einem Ort – tausche mühelos alle Vermögenswerte in Deiner Wallet + Tausche mit uns + Keine Fummeleien, keine Umsätze, keine blinden Flecken – Deine Transaktion ist immer geschützt + Undurchdringliche Verteidigung Maximiere Deine Wert mit Tarifen aus einem breiten Netzwerk vertrauenswürdiger Anbieter und wähle immer den Besten aus - Bester Preis + Unschlagbare Preise Problemlos und intuitiv, sodass Deine Token mit nur wenigen Handgriffen getauscht werden können - Einfacher als je zuvor + Einfach bequem Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers. Der Betrag beinhaltet:\n• Honorar des Dienstleisters\n• Netzwerkgebühr für das Senden %1$s von der Börse zurück an die Adresse des Benutzers. \n\n Provider-Slippage kann bis zu %2$s Der Betrag enthält die Gebühren des Dienstleisters. @@ -1089,6 +1091,8 @@ Gefällt dir Tangem? Du musst deinen Token zuordnen, bevor du Token erhalten kannst Netzmietgebühr erforderlich + Handlungsbedarf + Hast Du innerhalb von 7 Tagen nach dem Erstellen einer Wallet per App oder E-Mail Kontakt zum Support aufgenommen? Wenn ja oder wenn Du Dir nicht sicher bist, befolge die Anweisungen und führe diese vollständig aus. Vielen Dank! Alles erledigt! Keine weiteren Maßnahmen erforderlich. Sie werden jetzt auf die offizielle Tangem-Website weitergeleitet. Bitte lesen und befolgen Sie dort die Anweisungen. Hast Du das Tangem-Supportteam schon einmal direkt über diese Anwendung kontaktiert? diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index e8f5fd1f79..ada8a5bd4d 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -674,6 +674,8 @@ Escanee la tarjeta o el anillo para cambiar su configuración. Los cambios afectarán solo a la tarjeta o el anillo que haya escaneado y no afectarán a otros dispositivos vinculados a su billetera. ¡Prepare su tarjeta/anillo! Alerta de seguridad + No, no lo hice + Sí, guíame No tiene fondos suficientes en su saldo para vender esta criptomoneda. Deposite el activo deseado para continuar. Saldo insuficiente La venta de criptomonedas no está disponible en su región en este momento. Estamos trabajando activamente para ofrecerle esta opción pronto. ¡Manténgase conectado! @@ -881,16 +883,16 @@ La red cobrará una tarifa de aprobación de token para verificar que está autorizando el uso de su token para el swap. Intercambie más tokens a mejores tasas directamente en su billetera. ¡Nuevo proveedor de intercambio disponible! - Siéntase seguro con un soporte constante, lo que garantiza que sus transacciones se realicen sin problemas en cualquier momento - Asistencia 24 horas al día - Los proveedores de intercambio de confianza le permiten intercambiar activos sin esfuerzo, manteniendo todo seguro en su billetera + Siéntase seguro con una asistencia permanente que le ayudará con cualquier problema + Siempre aquí + Múltiples proveedores de confianza en un solo lugar: intercambie cualquier activo sin esfuerzo en su billetera Intercambie con nosotros - La seguridad de alto nivel y los proveedores verificados garantizan la protección de sus activos en cada intercambio - Más seguro que nunca + Sin cuelgues, sin pérdidas, sin puntos ciegos - su transacción siempre está protegida + Defensa impenetrable Maximice su valor con tarifas de una amplia red de proveedores de confianza, eligiendo siempre la mejor - Las mejores tarifas + Tarifas inmejorables Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques - Más fácil que nunca + Simplemente cómodo La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. El monto incluye:\n• tarifas del proveedor de servicios\n• tarifas de red por enviar %1$s desde el intercambio a la dirección del usuario. \n\nEl slippage del proveedor puede alcanzar el %2$s El importe incluye los honorarios del proveedor de servicios. @@ -978,7 +980,7 @@ Desbloquear todo con %s La blockchain está Inaccesible. Inténtelo más tarde Escanee la tarjeta o el anillo - Esta billetera ya ha sido activada anteriormente.\nSi no fue usted quien lo hizo, comuníquese con el servicio de asistencia.\nTangem nunca vende billeteras junto con un código de acceso generado previamente. + Esta billetera ya ha sido activada anteriormente.\nSi no fue usted quien lo hizo, comuníquese con el servicio de asistencia.\nTangem nunca vende billeteras con un código de acceso pre generado. Solicitud para firmar un mensaje.\n\n%s Dapp %1$s, solicitando\nfirmar transacción BNB.\n\n%2$s Orden de intercambio por %1$s\nPrecio: %2$s\nCantidad a recibir: %3$s\nCantidad a pagar: %4$s @@ -1089,6 +1091,8 @@ ¿Disfrutando de Tangem? Deba asociar su token antes de recibir tokens Se requiere tarifa de alquiler de red + Acción requerida + ¿Te pusiste en contacto con el servicio de asistencia a través de la aplicación o por correo electrónico en los 7 días posteriores a la creación de la billetera? Si lo hiciste o no estás seguro, sigue y completa las instrucciones. ¡Gracias! ¡Todo listo! No se requieren más acciones. Ahora será redirigido al sitio web oficial de Tangem. Por favor, lea y siga las instrucciones allí indicadas. ¿Alguna vez contactó al equipo de soporte de Tangem directamente desde esta aplicación? diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index fd85f80b66..529de205c8 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -271,6 +271,8 @@ Échange par %s Visitez le site Web du fournisseur pour obtenir un remboursement Opération échouée par le fournisseur + Votre échange prend plus de temps que prévu. Veuillez contacter le support du fournisseur pour obtenir de l\'aide. + Longue durée des transactions Le montant de la transaction a été remboursé en %1$s sur votre portefeuille en raison des règles OKX ou du pont. %2$s Le montant a été remboursé en %1$s (réseau %2$s) Visitez le site Web du fournisseur pour la vérification @@ -674,6 +676,8 @@ Scannez la carte pour modifier ses paramètres. Les modifications n\'affecteront que la carte que vous avez scannée et n\'affecteront pas les autres cartes liées à votre portefeuille. Préparez votre carte ! Alerte de sécurité + Non + Oui, guidez-moi Vous n\'avez pas assez de fonds sur votre solde pour vendre cette cryptomonnaie. Veuillez déposer l\'actif souhaité pour continuer. Solde insuffisant La vente de cryptomonnaies n\'est pas disponible dans votre région pour le moment. Nous travaillons activement pour vous proposer cette option prochainement. Restez connecté ! @@ -1089,6 +1093,8 @@ Vous appréciez Tangem ? Vous devez associer votre jeton avant de recevoir des jetons Frais de location de réseau requis + Action requise + Avez-vous contacté l\'assistance via l\'application ou par e-mail dans les 7 jours suivant la création d\'un portefeuille ? Si vous l\'avez fait ou si vous n\'êtes pas sûr, suivez et complétez les instructions. Merci ! Tout est en ordre ! Aucune autre action n\'est requise. Vous allez être redirigé vers le site officiel de Tangem. Veuillez lire et suivre les instructions qui y sont indiquées. Avez-vous déjà contacté l\'équipe de support Tangem directement depuis cette application ? @@ -1103,6 +1109,7 @@ Certains soldes de jetons n\'ont pas pu être mis à jour Il s\'agit d\'une carte Testnet. Elle ne peut pas traiter les transactions et ne doit être utilisée qu\'à des fins de test et de développement. À des fins de test uniquement + Le solde peut être obsolète. Rafraîchissez la page. Ignorer Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ? Oui, reprendre diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 931cb0fd78..9535f46439 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -268,6 +268,8 @@ %sによる交換 返金を受けるには、プロバイダーのウェブサイトにアクセスしてください。 プロバイダーによる操作が失敗しました。 + スワップに予想以上の時間がかかっています。プロバイダーサポートにお問い合わせください。 + 長い取引時間 OKXまたはブリッジのルールにより、取引金額は%1$sでウォレットに返金されました。%2$s 金額は %1$s(%2$sネットワーク)で返金されました 確認するには、プロバイダーのウェブサイトにアクセスしてください。 @@ -664,6 +666,8 @@ カードまたはリングをスキャンして設定を変更します。変更はスキャンしたカードまたはリングにのみ影響し、ウォレットに関連付けられている他のデバイスには影響しません。 Tangemを準備してください! セキュリティ警告 + いいえ、しませんでした + はい、案内してください 暗号通貨を売却するための十分な資金が残高にありません。続行するには希望の資産を入金してください。 残高不足 現在、お住まいの地域では暗号資産の売却はご利用いただけません。このオプションをすぐにご利用いただけるよう積極的に取り組んでいますので、お楽しみに! @@ -1078,6 +1082,8 @@ Tangemを楽しんでいますか? トークンを受け取る前に、トークンを関連付ける必要があります。 ネットワーク使用料が必要です + 対応が必要です + ウォレットを作成してから7日以内にアプリまたはEメール経由でサポートに連絡しましたか?連絡したか不明な場合は、手順に従って完了してください。 ありがとうございます。準備完了です。これ以上の操作は必要ありません。 Tangemの公式ウェブサイトに移動します。そちらの指示に従ってください。 このアプリ経由でTangemサポートチームに直接連絡したことがありますか? @@ -1092,6 +1098,7 @@ 一部のトークン残高を更新できませんでした これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。 テスト目的のみ + 残高が古い可能性があります。ページを更新してください。 破棄 バックアップが中断されました。再開しますか? はい、再開します diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b463199a6f..15e6dcf94d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -271,6 +271,8 @@ Exchange by %s Visit provider’s website to refund your money Operation failed by provider + Your swap is taking longer than expected. Please go to provider support for assistance. + Long transaction time The transaction amount was refunded in %1$s to your wallet due to OKX or bridge rules. %2$s The amount was refunded in %1$s (%2$s network) Visit provider’s website for verification @@ -813,6 +815,10 @@ Low staked balance A minimum of %1$s %2$s is required for restaking. Please top up your balance. Not enough %s + Insufficient Balance for Staking + A minimum of 3 ADA is required for restaking. Please top up your balance. + Not enough ADA + The minimum amount required for staking must exceed 5 ADA. Please top up your balance to start staking. Staking is currently unavailable due to network conditions. Please try again later. Staking on the %1$s network with a new validator will automatically transfer all previously staked funds to this validator. Reinvests your earned rewards in your staked amount, increasing potential earnings. @@ -918,6 +924,7 @@ You do not have funds to sell. Top up your account to be able to sell funds from it. You do not have funds to send. Top up your account to be able to send funds from it. Swapping %s is not supported by current providers, but we are working to add more options. + The displayed balance might be outdated due to caching. Selling funds will be available once the pending transaction(s) on the %s network is complete. Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not supported by current providers, but we are working to add more options. @@ -1107,6 +1114,7 @@ Some token balances could not be updated This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only + Balance may be outdated. Refresh the page. Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt index 6cbb851dc3..2c05dbfe7d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt @@ -3,9 +3,7 @@ package com.tangem.core.ui.components.text import android.content.res.Configuration import androidx.compose.animation.core.* import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* @@ -17,13 +15,13 @@ import com.tangem.core.ui.res.TangemThemePreview import kotlin.math.sqrt data class BladeAnimation( - val offset: Float, + val offsetState: State, ) @Composable fun rememberBladeAnimation(): BladeAnimation { val infiniteTransition = rememberInfiniteTransition() - val offset by infiniteTransition.animateFloat( + val offsetState = infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( @@ -31,15 +29,17 @@ fun rememberBladeAnimation(): BladeAnimation { ), ) - return BladeAnimation(offset) + return remember(offsetState) { + BladeAnimation(offsetState) + } } @Suppress("MagicNumber") @Composable fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle { - val offset = LocalBladeAnimation.current.offset - return if (isEnabled) { + val offset by LocalBladeAnimation.current.offsetState + val brush = remember(offset, textColor) { object : ShaderBrush() { override fun createShader(size: Size): Shader { @@ -55,7 +55,7 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle { colors = listOf(textColor.copy(alpha = 0.2f), textColor), from = baseStart + shift, to = baseEnd + shift, - colorStops = listOf(0.0f, 0.25f), + colorStops = listOf(0.0f, 0.15f), tileMode = TileMode.Mirror, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt index 76975fb223..9016ec1ccf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt @@ -39,6 +39,7 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool text = state.text.orMaskWithStars(isBalanceHidden), modifier = modifier, isAvailable = state.isAvailable, + isFlickering = state.isFlickering, ) } is TokenFiatAmountState.Loading -> { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 8bc21a0f65..be357d5ebf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -208,7 +208,11 @@ sealed class TokenItemState { ) } - data class TextContent(val text: String, val isAvailable: Boolean = true) : FiatAmountState() + data class TextContent( + val text: String, + val isAvailable: Boolean = true, + val isFlickering: Boolean = false, + ) : FiatAmountState() data object Loading : FiatAmountState() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index 340ca7d60d..adc1a8e4ca 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -3,6 +3,8 @@ package com.tangem.domain.tokens.model sealed class ScenarioUnavailabilityReason { data object None : ScenarioUnavailabilityReason() + data object UsedOutdatedData : ScenarioUnavailabilityReason() + // staking-specific data class StakingUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 804f5d06c5..767dfd45ab 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -115,15 +115,18 @@ sealed class TokenScreenAnalyticsEvent( fun ScenarioUnavailabilityReason.toReasonAnalyticsText(): String { return when (this) { - is ScenarioUnavailabilityReason.BuyUnavailable -> UNAVAILABLE + is ScenarioUnavailabilityReason.BuyUnavailable, + is ScenarioUnavailabilityReason.NotExchangeable, + is ScenarioUnavailabilityReason.NotSupportedBySellService, + is ScenarioUnavailabilityReason.StakingUnavailable, + ScenarioUnavailabilityReason.UnassociatedAsset, + -> UNAVAILABLE is ScenarioUnavailabilityReason.EmptyBalance -> EMPTY - ScenarioUnavailabilityReason.None -> "" - is ScenarioUnavailabilityReason.NotExchangeable -> UNAVAILABLE - is ScenarioUnavailabilityReason.NotSupportedBySellService -> UNAVAILABLE is ScenarioUnavailabilityReason.PendingTransaction -> PENDING - is ScenarioUnavailabilityReason.StakingUnavailable -> UNAVAILABLE - ScenarioUnavailabilityReason.UnassociatedAsset -> UNAVAILABLE - ScenarioUnavailabilityReason.Unreachable -> "" + ScenarioUnavailabilityReason.None, + ScenarioUnavailabilityReason.Unreachable, + ScenarioUnavailabilityReason.UsedOutdatedData, + -> "" } } } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index ccd2264205..623fbe5c1b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -59,4 +59,6 @@ sealed class CryptoCurrencyWarning { ) : CryptoCurrencyWarning() data object TokensInBetaWarning : CryptoCurrencyWarning() + + data object UsedOutdatedDataWarning : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 9b8dea0263..eafad782ed 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -52,14 +52,14 @@ class FetchTokenListUseCase( coroutineScope { val fetchStatuses = async { fetchNetworksStatuses( - userWalletId, - currencies.mapTo(hashSetOf()) { it.network }, + userWalletId = userWalletId, + networks = currencies.mapTo(hashSetOf()) { it.network }, refresh = mode.refreshNetworksStatuses, ) } val fetchQuotes = async { fetchQuotes( - currencies.mapTo(hashSetOf()) { it.id }, + currenciesIds = currencies.mapTo(hashSetOf()) { it.id }, refresh = mode.refreshQuotes, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt index 5a77f4953e..e6b029da6d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -1,15 +1,13 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,19 +17,14 @@ import kotlinx.coroutines.flow.* * Get crypto currency statuses by raw ID for all wallets * * @property currenciesRepository currencies repository - * @property quotesRepository quotes repository - * @property networksRepository networks repository - * @property stakingRepository staking repository * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ class GetAllWalletsCryptoCurrencyStatusesUseCase( private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, + private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { /** @@ -47,16 +40,8 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase( return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId, needFilterByAvailable) .flatMapLatest { userWalletsWithCurrencies: Map> -> val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) -> - val operations = CurrenciesStatusesOperations( - userWalletId = userWallet.walletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - val currencyStatusFlows = cryptoCurrencies.map { cryptoCurrency -> - operations.getCurrencyStatusFlow(cryptoCurrency) + currencyStatusOperations.getCurrencyStatusFlow(userWallet.walletId, cryptoCurrency) .map { it.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 02cfa6dc4d..c01b86587d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -2,15 +2,14 @@ package com.tangem.domain.tokens import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.StatusSource import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet @@ -30,36 +29,29 @@ class GetCryptoCurrencyActionsUseCase( private val rampManager: RampStateManager, private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val promoRepository: PromoRepository, private val dispatchers: CoroutineDispatcherProvider, private val swapFeatureToggles: SwapFeatureToggles, + private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { suspend operator fun invoke( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, ): Flow { - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - userWalletId = userWallet.walletId, - ) val networkId = cryptoCurrencyStatus.currency.network.id val requirements = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) { walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency) } return flow { val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + currencyStatusOperations.getNetworkCoinForSingleWalletWithTokenFlow(userWallet.walletId, networkId) } else if (!userWallet.isMultiCurrency) { - operations.getPrimaryCurrencyStatusFlow(includeQuotes = false) + currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWallet.walletId, includeQuotes = false) } else { - operations.getNetworkCoinFlow( + currencyStatusOperations.getNetworkCoinFlow( + userWalletId = userWallet.walletId, networkId = networkId, derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, includeQuotes = false, @@ -122,6 +114,10 @@ class GetCryptoCurrencyActionsUseCase( return getActionsForUnreachableCurrency(userWallet, cryptoCurrencyStatus, requirements) } + if (cryptoCurrencyStatus.value.source != StatusSource.ACTUAL) { + return getActionsForOutdatedData(userWallet, cryptoCurrencyStatus, requirements) + } + val activeList = mutableListOf() val disabledList = mutableListOf() @@ -266,6 +262,61 @@ class GetCryptoCurrencyActionsUseCase( return actionsList } + private suspend fun getActionsForOutdatedData( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + requirements: AssetRequirementsCondition?, + ): List { + val activeList = mutableListOf() + + // copy address + if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { + activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) + } + + // receive + if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { + val scenario = getReceiveScenario(requirements) + activeList.add(TokenActionsState.ActionState.Receive(scenario)) + } + + // swap + activeList.add( + TokenActionsState.ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.UsedOutdatedData, + showBadge = false, + ), + ) + + // buy + if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrencyStatus.currency)) { + activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) + } else { + activeList.add( + TokenActionsState.ActionState.Buy( + ScenarioUnavailabilityReason.BuyUnavailable( + cryptoCurrencyName = cryptoCurrencyStatus.currency.name, + ), + ), + ) + } + + // staking + activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.UsedOutdatedData, null)) + + // send + activeList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.UsedOutdatedData)) + + // region sell + activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.UsedOutdatedData)) + // endregion + + // hide + activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) + + return activeList + } + private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason { return if (requirements is AssetRequirementsCondition.PaidTransaction || requirements is AssetRequirementsCondition.PaidTransactionWithFee diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt index e69c36858f..20354b45c4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -1,24 +1,15 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider class GetCryptoCurrencyStatusSyncUseCase( - internal val currenciesRepository: CurrenciesRepository, - internal val quotesRepository: QuotesRepository, - internal val networksRepository: NetworksRepository, - internal val stakingRepository: StakingRepository, - internal val dispatchers: CoroutineDispatcherProvider, + private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { // multi-currency @@ -27,29 +18,13 @@ class GetCryptoCurrencyStatusSyncUseCase( cryptoCurrencyId: CryptoCurrency.ID, isSingleWalletWithTokens: Boolean = false, ): Either { - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - - return operations.getCurrencyStatusSync(cryptoCurrencyId, isSingleWalletWithTokens) + return currencyStatusOperations.getCurrencyStatusSync(userWalletId, cryptoCurrencyId, isSingleWalletWithTokens) .mapLeft { error -> error.mapToCurrencyError() } } // single-currency suspend operator fun invoke(userWalletId: UserWalletId): Either { - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - - return operations.getPrimaryCurrencyStatusSync() + return currencyStatusOperations.getPrimaryCurrencyStatusSync(userWalletId) .mapLeft { error -> error.mapToCurrencyError() } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt index 4c3e6b1b71..590cb6caa0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt @@ -1,35 +1,18 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider class GetCryptoCurrencyStatusesSyncUseCase( - internal val currenciesRepository: CurrenciesRepository, - internal val quotesRepository: QuotesRepository, - internal val networksRepository: NetworksRepository, - internal val stakingRepository: StakingRepository, - internal val dispatchers: CoroutineDispatcherProvider, + private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either> { - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - - return operations.getCurrenciesStatusesSync() + return currencyStatusOperations.getCurrenciesStatusesSync(userWalletId) .mapLeft { error -> error.mapToTokenListError() } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index f74a62b2d4..0595b091fe 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -1,15 +1,12 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -17,15 +14,9 @@ import kotlinx.coroutines.flow.* /** * Use case for fetching the status of a cryptocurrency associated with a user wallet. * - * @property currenciesRepository Repository for managing and fetching cryptocurrencies. - * @property quotesRepository Repository for managing and fetching cryptocurrency quotes. - * @property networksRepository Repository for managing and fetching information related to blockchain networks. */ class GetCurrencyStatusUpdatesUseCase( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, + private val currencyStatusOperations: BaseCurrencyStatusOperations, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -58,19 +49,12 @@ class GetCurrencyStatusUpdatesUseCase( currencyId: CryptoCurrency.ID, isSingleWalletWithTokens: Boolean, ): Flow> { - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, + val currencyFlow = currencyStatusOperations.getCurrencyStatusFlow( userWalletId = userWalletId, + currencyId = currencyId, + isSingleWalletWithTokens = isSingleWalletWithTokens, ) - val currencyFlow = if (isSingleWalletWithTokens) { - operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) - } else { - operations.getCurrencyStatusFlow(currencyId) - } return currencyFlow.map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 111f62b3ce..0368ba6b53 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,15 +1,14 @@ package com.tangem.domain.tokens -import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.domain.tokens.model.warnings.KaspaWarnings -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -22,11 +21,10 @@ import java.math.BigDecimal class GetCurrencyWarningsUseCase( private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, private val currencyChecksRepository: CurrencyChecksRepository, + private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { suspend operator fun invoke( @@ -36,18 +34,11 @@ class GetCurrencyWarningsUseCase( isSingleWalletWithTokens: Boolean, ): Flow> { val currency = currencyStatus.currency - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - userWalletId = userWalletId, - ) + // don't add here notifications that require async requests return combine( getCoinRelatedWarnings( userWalletId = userWalletId, - operations = operations, networkId = currency.network.id, currencyId = currency.id, derivationPath = derivationPath, @@ -74,22 +65,23 @@ class GetCurrencyWarningsUseCase( @Suppress("LongParameterList") private suspend fun getCoinRelatedWarnings( userWalletId: UserWalletId, - operations: CurrenciesStatusesOperations, networkId: Network.ID, currencyId: CryptoCurrency.ID, derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Flow> { - val currencyFlow = if (isSingleWalletWithTokens) { - operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) - } else { - operations.getCurrencyStatusFlow(currencyId) - } + val currencyFlow = currencyStatusOperations.getCurrencyStatusFlow( + userWalletId = userWalletId, + currencyId = currencyId, + isSingleWalletWithTokens = isSingleWalletWithTokens, + ) + val networkFlow = if (isSingleWalletWithTokens) { - operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + currencyStatusOperations.getNetworkCoinForSingleWalletWithTokenFlow(userWalletId, networkId) } else { - operations.getNetworkCoinFlow(networkId, derivationPath) + currencyStatusOperations.getNetworkCoinFlow(userWalletId, networkId, derivationPath) } + return combine( currencyFlow.map { it.getOrNull() }, networkFlow.map { it.getOrNull() }, @@ -97,6 +89,7 @@ class GetCurrencyWarningsUseCase( when { tokenStatus != null && coinStatus != null -> { buildList { + getUsedOutdatedDataWarning(tokenStatus)?.let(::add) getIsBetaTokensWarning(tokenStatus.currency)?.let(::add) getFeeWarning( userWalletId = userWalletId, @@ -144,6 +137,10 @@ class GetCurrencyWarningsUseCase( } } + private fun getUsedOutdatedDataWarning(status: CryptoCurrencyStatus): CryptoCurrencyWarning? { + return CryptoCurrencyWarning.UsedOutdatedDataWarning.takeIf { status.value.source == StatusSource.ONLY_CACHE } + } + private fun getIsBetaTokensWarning(currency: CryptoCurrency): CryptoCurrencyWarning? { val isTokenBetaFunctionality = BlockchainUtils.isTokenBetaFunctionality(currency.network.id.value) return if (currency is CryptoCurrency.Token && isTokenBetaFunctionality) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt index a66a4c3074..0e52779600 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt @@ -2,23 +2,16 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider class GetFeePaidCryptoCurrencyStatusSyncUseCase( internal val currenciesRepository: CurrenciesRepository, - internal val quotesRepository: QuotesRepository, - internal val networksRepository: NetworksRepository, - internal val stakingRepository: StakingRepository, - internal val dispatchers: CoroutineDispatcherProvider, + private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { suspend operator fun invoke( @@ -28,24 +21,20 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase( val cryptoCurrency = cryptoCurrencyStatus.currency val network = cryptoCurrency.network val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, network) - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) return either { when (feePaidCurrency) { - FeePaidCurrency.Coin -> - operations - .getNetworkCoinSync(network.id, network.derivationPath) + is FeePaidCurrency.Coin -> { + currencyStatusOperations.getNetworkCoinSync(userWalletId, network.id, network.derivationPath) .getOrNull() - FeePaidCurrency.SameCurrency, + } + is FeePaidCurrency.Token -> { + currencyStatusOperations.getCurrencyStatusSync(userWalletId, feePaidCurrency.tokenId) + .getOrNull() + } + is FeePaidCurrency.SameCurrency, is FeePaidCurrency.FeeResource, -> cryptoCurrencyStatus - is FeePaidCurrency.Token -> operations.getCurrencyStatusSync(feePaidCurrency.tokenId).getOrNull() } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index bd77e54e3b..0db0f74c26 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -1,24 +1,18 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* class GetNetworkCoinStatusUseCase( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, + private val currencyStatusOperations: BaseCurrencyStatusOperations, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -47,17 +41,10 @@ class GetNetworkCoinStatusUseCase( derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Either { - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - userWalletId = userWalletId, - ) val maybeCurrency = if (isSingleWalletWithTokens) { - operations.getNetworkCoinForSingleWalletWithTokenSync(networkId) + currencyStatusOperations.getNetworkCoinForSingleWalletWithTokenSync(userWalletId, networkId) } else { - operations.getNetworkCoinSync(networkId, derivationPath) + currencyStatusOperations.getNetworkCoinSync(userWalletId, networkId, derivationPath) } return maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } @@ -68,17 +55,10 @@ class GetNetworkCoinStatusUseCase( derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Flow> { - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - userWalletId = userWalletId, - ) val networkFlow = if (isSingleWalletWithTokens) { - operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + currencyStatusOperations.getNetworkCoinForSingleWalletWithTokenFlow(userWalletId, networkId) } else { - operations.getNetworkCoinFlow(networkId, derivationPath) + currencyStatusOperations.getNetworkCoinFlow(userWalletId, networkId, derivationPath) } return networkFlow.map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt index fdc6dfaf8a..000411bff6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt @@ -1,14 +1,11 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -16,17 +13,11 @@ import kotlinx.coroutines.flow.* /** * Use case for fetching the status of the primary cryptocurrency associated with a user wallet. * - * @property currenciesRepository Repository for managing and fetching cryptocurrencies. - * @property quotesRepository Repository for managing and fetching cryptocurrency quotes. - * @property networksRepository Repository for managing and fetching information related to blockchain networks. * @property dispatchers Provides coroutine dispatchers. */ class GetPrimaryCurrencyStatusUpdatesUseCase( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, + private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { /** @@ -44,15 +35,7 @@ class GetPrimaryCurrencyStatusUpdatesUseCase( private suspend fun getPrimaryCurrency( userWalletId: UserWalletId, ): Flow> { - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - userWalletId = userWalletId, - ) - - return operations.getPrimaryCurrencyStatusFlow().map { maybeCurrency -> + return currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWalletId).map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index f2a7df9a80..98e325348e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -4,17 +4,13 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.core.utils.toLce -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.operations.CurrenciesStatusesCachedOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesLceOperations +import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emitAll @@ -23,45 +19,27 @@ import kotlinx.coroutines.flow.transformLatest class GetTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, - private val tokensFeatureToggles: TokensFeatureToggles, + private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, ) { @OptIn(ExperimentalCoroutinesApi::class) fun launch(userWalletId: UserWalletId): LceFlow { - val statusesFlow = if (tokensFeatureToggles.isBalancesCachingEnabled) { - CurrenciesStatusesCachedOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ).getCurrenciesStatuses(userWalletId) - } else { - CurrenciesStatusesLceOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ).getCurrenciesStatuses(userWalletId) - } - - return statusesFlow.transformLatest { maybeCurrencies -> - maybeCurrencies.fold( - ifLoading = { maybeContent -> - if (maybeContent != null) { - emitAll(createTokenListLce(userWalletId, maybeContent, isCurrenciesLoading = true)) - } else { - emit(lceLoading()) - } - }, - ifContent = { content -> - emitAll(createTokenListLce(userWalletId, content, isCurrenciesLoading = false)) - }, - ifError = { error -> emit(error.lceError()) }, - ) - } + return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId) + .transformLatest { maybeCurrencies -> + maybeCurrencies.fold( + ifLoading = { maybeContent -> + if (maybeContent != null) { + emitAll(createTokenListLce(userWalletId, maybeContent, isCurrenciesLoading = true)) + } else { + emit(lceLoading()) + } + }, + ifContent = { content -> + emitAll(createTokenListLce(userWalletId, content, isCurrenciesLoading = false)) + }, + ifError = { error -> emit(error.lceError()) }, + ) + } } private fun createTokenListLce( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index d76493e741..5f874b7419 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -7,15 +7,11 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceLoading -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance -import com.tangem.domain.tokens.operations.CurrenciesStatusesLceOperations +import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine @@ -24,10 +20,7 @@ import kotlinx.coroutines.flow.transformLatest import timber.log.Timber class GetWalletTotalBalanceUseCase( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, + private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, ) { operator fun invoke( @@ -63,13 +56,12 @@ class GetWalletTotalBalanceUseCase( @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(userWalletId: UserWalletId): LceFlow { - val currenciesStatuses = getStatuses(userWalletId) + return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId) + .transformLatest { maybeStatuses -> + val balance = createBalance(maybeStatuses) - return currenciesStatuses.transformLatest { maybeStatuses -> - val balance = createBalance(maybeStatuses) - - emit(balance) - } + emit(balance) + } } private fun createBalance( @@ -84,15 +76,4 @@ class GetWalletTotalBalanceUseCase( operations.calculateFiatBalance() } - - private fun getStatuses(userWalletId: UserWalletId): LceFlow> { - val operations = CurrenciesStatusesLceOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - - return operations.getCurrenciesStatuses(userWalletId) - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt new file mode 100644 index 0000000000..b777dbc595 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.tokens.operations + +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Base operations for working with currencies statuses + * +[REDACTED_AUTHOR] + */ +interface BaseCurrenciesStatusesOperations { + + /** Get [LceFlow] of currencies statuses by [userWalletId] */ + fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt new file mode 100644 index 0000000000..474974bf82 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -0,0 +1,366 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.* +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.recover +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.* + +/** + * Base operations for working with currency status + * + * @property currenciesRepository repository for currencies + * @property quotesRepository repository for quotes + * @property networksRepository repository for networks + * @property stakingRepository repository for staking + * +[REDACTED_AUTHOR] + */ +abstract class BaseCurrencyStatusOperations( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, +) { + + protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository) + + protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> + + protected abstract fun getNetworksStatuses( + userWalletId: UserWalletId, + network: Network, + ): EitherFlow> + + suspend fun getCurrencyStatusFlow( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + isSingleWalletWithTokens: Boolean, + ): Flow> { + val currency = recover( + block = { + if (isSingleWalletWithTokens) { + getSingleCurrencyWalletWithCardTokensCurrency(userWalletId, currencyId) + } else { + getMultiCurrencyWalletCurrency(userWalletId, currencyId) + } + }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(userWalletId = userWalletId, currency = currency) + } + + fun getCurrencyStatusFlow( + userWalletId: UserWalletId, + currency: CryptoCurrency, + includeQuotes: Boolean = true, + ): Flow> { + val rawCurrencyId = currency.id.rawCurrencyId + + val quoteFlow = if (includeQuotes && rawCurrencyId != null) { + getQuotes(rawCurrencyId) + .map { maybeQuotes -> + maybeQuotes.flatMap { quotes -> + quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }?.right() + ?: Error.EmptyQuotes.left() + } + } + } else { + // don't use emptyFlow() + flow { emit(Error.EmptyQuotes.left()) } + } + + val statusFlow = getNetworksStatuses(userWalletId = userWalletId, network = currency.network) + .map { maybeStatuses -> + maybeStatuses.flatMap { statuses -> + statuses.singleOrNull { it.network == currency.network }?.right() + ?: Error.EmptyNetworksStatuses.left() + } + } + + val yieldBalanceFlow = getYieldBalance(userWalletId = userWalletId, cryptoCurrency = currency) + + return combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> + currencyStatusProxyCreator.createCurrencyStatus( + currency = currency, + maybeQuote = maybeQuote, + maybeNetworkStatus = maybeNetworkStatus, + maybeYieldBalance = maybeYieldBalance, + ) + } + } + + suspend fun getNetworkCoinFlow( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + includeQuotes: Boolean = true, + ): Flow> { + val currency = recover( + block = { getNetworkCoin(userWalletId, networkId, derivationPath) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(userWalletId, currency, includeQuotes) + } + + suspend fun getNetworkCoinForSingleWalletWithTokenFlow( + userWalletId: UserWalletId, + networkId: Network.ID, + ): Flow> { + val currency = recover( + block = { getNetworkCoinForSingleWalletWithToken(userWalletId, networkId) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(userWalletId, currency) + } + + suspend fun getNetworkCoinSync( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): Either { + val currency = recover( + block = { getNetworkCoin(userWalletId, networkId, derivationPath) }, + recover = { return it.left() }, + ) + + return getCurrencyStatusSync(userWalletId, currency.id) + } + + suspend fun getCurrencyStatusSync( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + isSingleWalletWithTokens: Boolean = false, + ): Either { + return either { + catch( + block = { + val currency = if (isSingleWalletWithTokens) { + currenciesRepository.getSingleCurrencyWalletWithCardCurrency(userWalletId, cryptoCurrencyId) + } else { + currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId) + } + val quote = cryptoCurrencyId.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }?.right() + ?: Error.EmptyQuotes.left() + val networkStatuses = + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = setOf(currency.network), + refresh = false, + ).firstOrNull { it.network == currency.network }.right() + + val yieldBalances = getYieldBalanceSync(userWalletId, currency) + + return currencyStatusProxyCreator.createCurrencyStatus( + currency = currency, + maybeQuote = quote, + maybeNetworkStatus = networkStatuses, + maybeYieldBalance = yieldBalances, + ) + }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + + suspend fun getNetworkCoinForSingleWalletWithTokenSync( + userWalletId: UserWalletId, + networkId: Network.ID, + ): Either = either { + val currency = getNetworkCoinForSingleWalletWithToken(userWalletId, networkId) + + return getCurrencyStatusSync(userWalletId, currency.id) + } + + suspend fun getPrimaryCurrencyStatusFlow( + userWalletId: UserWalletId, + includeQuotes: Boolean = true, + ): Flow> { + val currency = recover( + block = { getPrimaryCurrency(userWalletId) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(userWalletId, currency, includeQuotes) + } + + suspend fun getCurrenciesStatusesSync(userWalletId: UserWalletId): Either> { + return either { + catch( + block = { + val nonEmptyCurrencies = + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() + ?: return emptyList().right() + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() + val quotes = quotesRepository.getQuotesSync(rawIds, false).right() + val networkStatuses = + networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() + val yieldBalances = getYieldBalancesSync(userWalletId, nonEmptyCurrencies) + + return currencyStatusProxyCreator.createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = quotes, + maybeNetworkStatuses = networkStatuses, + maybeYieldBalances = yieldBalances, + ) + }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + + suspend fun getPrimaryCurrencyStatusSync(userWalletId: UserWalletId): Either = either { + val currency = catch( + block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + val quotes = catch( + block = { + currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) } + ?.right() ?: Error.EmptyQuotes.left() + }, + catch = { Error.DataError(it).left() }, + ) + val networkStatus = catch( + block = { + networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network)) + .firstOrNull { it.network == currency.network } + .right() + }, + catch = { Error.DataError(it).left() }, + ) + val yieldBalances = getYieldBalanceSync(userWalletId, currency) + + return currencyStatusProxyCreator.createCurrencyStatus( + currency = currency, + maybeQuote = quotes, + maybeNetworkStatus = networkStatus, + maybeYieldBalance = yieldBalances, + ) + } + + private suspend fun Raise.getMultiCurrencyWalletCurrency( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + ): CryptoCurrency { + return Either.catch { + currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId = userWalletId, id = currencyId) + } + .mapLeft(Error::DataError) + .bind() + } + + private suspend fun Raise.getSingleCurrencyWalletWithCardTokensCurrency( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + ): CryptoCurrency { + return Either.catch { currenciesRepository.getSingleCurrencyWalletWithCardCurrency(userWalletId, currencyId) } + .mapLeft { Error.DataError(it) } + .bind() + } + + private fun getYieldBalance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): EitherFlow { + return stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } + } + + private suspend fun Raise.getNetworkCoin( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency { + return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) } + .mapLeft { Error.DataError(it) } + .bind() + } + + private suspend fun Raise.getNetworkCoinForSingleWalletWithToken( + userWalletId: UserWalletId, + networkId: Network.ID, + ): CryptoCurrency { + return Either.catch { + currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId) + .find { it.network.id == networkId && it is CryptoCurrency.Coin } + ?: raise(Error.DataError(IllegalStateException("Coin with network $networkId not found for this card"))) + } + .mapLeft { Error.DataError(it) } + .bind() + } + + private suspend fun getYieldBalancesSync( + userWalletId: UserWalletId, + cryptoCurrencies: List, + ): Either { + return catch( + block = { + stakingRepository.getMultiYieldBalanceSync( + userWalletId, + cryptoCurrencies, + ).right() + }, + catch = { + Error.EmptyYieldBalances.left() + }, + ) + } + + private suspend fun getYieldBalanceSync( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Either { + return catch( + block = { + stakingRepository.getSingleYieldBalanceSync( + userWalletId, + cryptoCurrency, + ).right() + }, + catch = { + Error.EmptyYieldBalances.left() + }, + ) + } + + private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + return catch( + block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + } + + protected fun getIds(currencies: List): Pair, NonEmptySet> { + val currencyIdToNetworkId = currencies.associate { currency -> + currency.id to currency.network + } + val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() + val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() + + requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } + requireNotNull(networks) { "Networks IDs cannot be empty" } + + return networks to currenciesIds + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesCachedOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt similarity index 76% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesCachedOperations.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 70fff8b1da..cd0a3639c9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesCachedOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -1,10 +1,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.ensureNotNull -import arrow.core.raise.recover +import arrow.core.raise.* import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce @@ -15,37 +12,55 @@ import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.utils.extractAddress import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -internal class CurrenciesStatusesCachedOperations( +class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, -) { +) : BaseCurrenciesStatusesOperations, + BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { - fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> { + override fun getCurrenciesStatuses( + userWalletId: UserWalletId, + ): LceFlow> { return transformToCurrenciesStatuses( userWalletId = userWalletId, currenciesFlow = getCurrencies(userWalletId), ) } + @OptIn(ExperimentalCoroutinesApi::class) private fun transformToCurrenciesStatuses( userWalletId: UserWalletId, currenciesFlow: EitherFlow>, ): LceFlow> = lceFlow { - currenciesFlow.collectLatest { maybeCurrencies -> + currenciesFlow.flatMapLatest { maybeCurrencies -> val isUpdating = MutableStateFlow(value = true) val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull() ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens } + // This is only 'true' when the flow here is empty, such as during initial loading + if (isLoading.get()) { + val loadingCurrencies = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + maybeYieldBalances = null, + isUpdating = true, + ) + send(loadingCurrencies) + } + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) fun createCurrenciesStatuses( @@ -61,6 +76,9 @@ internal class CurrenciesStatusesCachedOperations( isUpdating = isUpdating, ) + launch { fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies) } + .invokeOnCompletion { isUpdating.value = false } + combine( flow = getQuotes(currenciesIds), flow2 = getNetworksStatuses(userWalletId, networks), @@ -69,17 +87,9 @@ internal class CurrenciesStatusesCachedOperations( transform = ::createCurrenciesStatuses, ) .distinctUntilChanged() - .onEach { maybeCurrenciesStatuses -> - send(maybeCurrenciesStatuses) - } - .launchIn(scope = this) - - launch { - fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies) - }.invokeOnCompletion { - isUpdating.value = false - } } + .onEach(::send) + .launchIn(scope = this) } private suspend fun Raise.fetchComponents( @@ -131,7 +141,7 @@ internal class CurrenciesStatusesCachedOperations( val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus) - val currencyStatus = createCurrencyStatus( + val currencyStatus = currencyStatusProxyCreator.createCurrencyStatus( currency = currency, quote = quote, networkStatus = networkStatus, @@ -160,24 +170,6 @@ internal class CurrenciesStatusesCachedOperations( ) } - private fun createCurrencyStatus( - currency: CryptoCurrency, - quote: Quote?, - networkStatus: NetworkStatus?, - yieldBalance: YieldBalance?, - ignoreQuote: Boolean, - ): CryptoCurrencyStatus { - val currencyStatusOperations = CurrencyStatusOperations( - currency = currency, - quote = quote, - networkStatus = networkStatus, - yieldBalance = yieldBalance, - ignoreQuote = ignoreQuote, - ) - - return currencyStatusOperations.createTokenStatus() - } - private fun getCurrencies(userWalletId: UserWalletId): EitherFlow> { return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) .map, Either>> { it.right() } @@ -197,6 +189,33 @@ internal class CurrenciesStatusesCachedOperations( .distinctUntilChanged() } + override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { + return quotesRepository.getQuotesUpdates(setOf(id)) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(Error.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } + + override fun getNetworksStatuses( + userWalletId: UserWalletId, + network: Network, + ): EitherFlow> { + return networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network)) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(Error.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } + private fun getNetworksStatuses( userWalletId: UserWalletId, networks: NonEmptySet, @@ -227,28 +246,6 @@ internal class CurrenciesStatusesCachedOperations( .distinctUntilChanged() } - private fun getIds(currencies: List): Pair, NonEmptySet> { - val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.network - } - val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() - val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() - - requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } - requireNotNull(networks) { "Networks IDs cannot be empty" } - - return networks to currenciesIds - } - - private fun extractAddress(networkStatus: NetworkStatus?): String? { - return when (val value = networkStatus?.value) { - is NetworkStatus.NoAccount -> value.address.defaultAddress.value - is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value - is NetworkStatus.Verified -> value.address.defaultAddress.value - else -> null - } - } - companion object { private const val RETRY_DELAY = 2000L } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index eb0e4023b1..8178ee7ecd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -1,355 +1,29 @@ package com.tangem.domain.tokens.operations -import arrow.core.* -import arrow.core.raise.* +import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.* -// FIXME: Refactor - [REDACTED_JIRA] -@Suppress("LargeClass") -internal class CurrenciesStatusesOperations( - private val currenciesRepository: CurrenciesRepository, +class CurrenciesStatusesOperations( + currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, - private val userWalletId: UserWalletId, -) { + stakingRepository: StakingRepository, +) : BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { - suspend fun getCurrenciesStatusesSync(): Either> { - return either { - catch( - block = { - val nonEmptyCurrencies = - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() - ?: return emptyList().right() - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() - val quotes = quotesRepository.getQuotesSync(rawIds, false).right() - val networkStatuses = - networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() - val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies) - - return createCurrenciesStatuses( - nonEmptyCurrencies, - quotes, - networkStatuses, - yieldBalances, - ) - }, - catch = { raise(Error.DataError(it)) }, - ) - } - } - - suspend fun getCurrencyStatusSync( - cryptoCurrencyId: CryptoCurrency.ID, - isSingleWalletWithTokens: Boolean = false, - ): Either { - return either { - catch( - block = { - val currency = if (isSingleWalletWithTokens) { - currenciesRepository.getSingleCurrencyWalletWithCardCurrency(userWalletId, cryptoCurrencyId) - } else { - currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId) - } - val quote = cryptoCurrencyId.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }?.right() - ?: Error.EmptyQuotes.left() - val networkStatuses = - networksRepository.getNetworkStatusesSync( - userWalletId, - setOf(currency.network), - false, - ).firstOrNull { - it.network == currency.network - }.right() - val yieldBalances = getYieldBalanceSync(currency) - - return createCurrencyStatus(currency, quote, networkStatuses, yieldBalances) - }, - catch = { raise(Error.DataError(it)) }, - ) - } - } - - suspend fun getNetworkCoinSync( - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): Either { - val currency = recover( - block = { getNetworkCoin(networkId, derivationPath) }, - recover = { return it.left() }, - ) - - return getCurrencyStatusSync(currency.id) - } - - suspend fun getNetworkCoinForSingleWalletWithTokenSync( - networkId: Network.ID, - ): Either = either { - val currency = getNetworkCoinForSingleWalletWithToken(networkId) - - return getCurrencyStatusSync(currency.id) - } - - suspend fun getPrimaryCurrencyStatusSync(): Either = either { - val currency = catch( - block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, - catch = { raise(Error.DataError(it)) }, - ) - val quotes = catch( - block = { - currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) } - ?.right() ?: Error.EmptyQuotes.left() - }, - catch = { Error.DataError(it).left() }, - ) - val networkStatus = catch( - block = { - networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network)) - .firstOrNull { it.network == currency.network } - .right() - }, - catch = { Error.DataError(it).left() }, - ) - val yieldBalances = getYieldBalanceSync(currency) - - return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances) - } - - suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { - val currency = recover( - block = { getMultiCurrencyWalletCurrency(currencyId) }, - recover = { return flowOf(it.left()) }, - ) - - return getCurrencyStatusFlow(currency) - } - - suspend fun getCurrencyStatusSingleWalletWithTokensFlow( - currencyId: CryptoCurrency.ID, - ): Flow> { - val currency = recover( - block = { getSingleCurrencyWalletWithCardTokensCurrency(currencyId) }, - recover = { return flowOf(it.left()) }, - ) - - return getCurrencyStatusFlow(currency) - } - - suspend fun getNetworkCoinFlow( - networkId: Network.ID, - derivationPath: Network.DerivationPath, - includeQuotes: Boolean = true, - ): Flow> { - val currency = recover( - block = { getNetworkCoin(networkId, derivationPath) }, - recover = { return flowOf(it.left()) }, - ) - - return getCurrencyStatusFlow(currency, includeQuotes) - } - - suspend fun getNetworkCoinForSingleWalletWithTokenFlow( - networkId: Network.ID, - ): Flow> { - val currency = recover( - block = { getNetworkCoinForSingleWalletWithToken(networkId) }, - recover = { return flowOf(it.left()) }, - ) - - return getCurrencyStatusFlow(currency) - } - - suspend fun getPrimaryCurrencyStatusFlow(includeQuotes: Boolean = true): Flow> { - val currency = recover( - block = { getPrimaryCurrency() }, - recover = { return flowOf(it.left()) }, - ) - - return getCurrencyStatusFlow(currency, includeQuotes) - } - - fun getCurrencyStatusFlow( - currency: CryptoCurrency, - includeQuotes: Boolean = true, - ): Flow> { - val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) - - val quoteFlow = if (includeQuotes) { - getQuotes(currenciesIds) - .map { maybeQuotes -> - maybeQuotes.flatMap { quotes -> - quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }?.right() - ?: Error.EmptyQuotes.left() - } - } - } else { - // don't use emptyFlow() - flow { emit(Error.EmptyQuotes.left()) } - } - - val statusFlow = getNetworksStatuses(networks) - .map { maybeStatuses -> - maybeStatuses.flatMap { statuses -> - statuses.singleOrNull { it.network == currency.network }?.right() - ?: Error.EmptyNetworksStatuses.left() - } - } - - val yieldBalanceFlow = getYieldBalance(currency) - - return combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> - createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus, maybeYieldBalance) - } - } - - private fun createCurrenciesStatuses( - currencies: NonEmptyList, - maybeQuotes: Either>?, - maybeNetworkStatuses: Either>?, - maybeYieldBalances: Either?, - ): Either> = either { - var quotesRetrievingFailed = false - - val networksStatuses = maybeNetworkStatuses?.bind()?.toNonEmptySetOrNull() - val quotes: Set? = maybeQuotes?.fold( - ifLeft = { - quotesRetrievingFailed = true - null - }, - ifRight = { - it.ifEmpty { - quotesRetrievingFailed = true - null - } - }, - ) - - val yieldBalances = maybeYieldBalances?.getOrNull() - - currencies.map { currency -> - val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val address = extractAddress(networkStatus) - - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - val yieldBalance = if (supportedIntegration.isNullOrEmpty().not()) { - (yieldBalances as? YieldBalanceList.Data)?.getBalance( - address = address, - integrationId = supportedIntegration, - ) - } else { - null - } - createCurrencyStatus( - currency = currency, - quote = quote, - networkStatus = networkStatus, - ignoreQuote = quotesRetrievingFailed, - yieldBalance = yieldBalance, - ) - } - } - - private fun createCurrencyStatus( - currency: CryptoCurrency, - maybeQuote: Either, - maybeNetworkStatus: Either, - maybeYieldBalance: Either?, - ): Either = either { - var quoteRetrievingFailed = false - - val networkStatus = maybeNetworkStatus.bind() - val quote = recover({ maybeQuote.bind() }) { - quoteRetrievingFailed = true - null - } - val yieldBalance = maybeYieldBalance?.getOrNull() - - createCurrencyStatus( - currency = currency, - quote = quote, - networkStatus = networkStatus, - ignoreQuote = quoteRetrievingFailed, - yieldBalance = yieldBalance, - ) - } - - private fun createCurrencyStatus( - currency: CryptoCurrency, - quote: Quote?, - networkStatus: NetworkStatus?, - ignoreQuote: Boolean, - yieldBalance: YieldBalance?, - ): CryptoCurrencyStatus { - val currencyStatusOperations = CurrencyStatusOperations( - currency = currency, - quote = quote, - networkStatus = networkStatus, - ignoreQuote = ignoreQuote, - yieldBalance = yieldBalance, - ) - - return currencyStatusOperations.createTokenStatus() - } - - private suspend fun Raise.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency { - return Either.catch { - currenciesRepository.getMultiCurrencyWalletCurrency( - userWalletId = userWalletId, - id = currencyId, - ) - } - .mapLeft { Error.DataError(it) } - .bind() - } - - private suspend fun Raise.getSingleCurrencyWalletWithCardTokensCurrency( - currencyId: CryptoCurrency.ID, - ): CryptoCurrency { - return Either.catch { currenciesRepository.getSingleCurrencyWalletWithCardCurrency(userWalletId, currencyId) } - .mapLeft { Error.DataError(it) } - .bind() - } - - private suspend fun Raise.getNetworkCoin( - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency { - return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) } - .mapLeft { Error.DataError(it) } - .bind() - } - - private suspend fun Raise.getNetworkCoinForSingleWalletWithToken(networkId: Network.ID): CryptoCurrency { - return Either.catch { - currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId) - .find { it.network.id == networkId && it is CryptoCurrency.Coin } - ?: raise(Error.DataError(IllegalStateException("Coin with network $networkId not found for this card"))) - } - .mapLeft { Error.DataError(it) } - .bind() - } - - private suspend fun Raise.getPrimaryCurrency(): CryptoCurrency { - return catch( - block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, - catch = { raise(Error.DataError(it)) }, - ) - } - - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - val rawIds = tokensIds.mapNotNull { it.rawCurrencyId }.toSet() - return quotesRepository.getQuotesUpdatesLegacy(rawIds) + override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { + return quotesRepository.getQuotesUpdatesLegacy(setOf(id)) .map, Either>> { quotes -> if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right() } @@ -359,78 +33,16 @@ internal class CurrenciesStatusesOperations( } } - private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { - return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, networks) + override fun getNetworksStatuses( + userWalletId: UserWalletId, + network: Network, + ): EitherFlow> { + return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, setOf(network)) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } - private suspend fun getYieldBalancesSync( - cryptoCurrencies: List, - ): Either { - return catch( - block = { - stakingRepository.getMultiYieldBalanceSync( - userWalletId, - cryptoCurrencies, - ).right() - }, - catch = { - Error.EmptyYieldBalances.left() - }, - ) - } - - private suspend fun getYieldBalanceSync( - cryptoCurrency: CryptoCurrency, - ): Either { - return catch( - block = { - stakingRepository.getSingleYieldBalanceSync( - userWalletId, - cryptoCurrency, - ).right() - }, - catch = { - Error.EmptyYieldBalances.left() - }, - ) - } - - private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow { - return stakingRepository.getSingleYieldBalanceFlow( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - ).map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } - } - - private fun getIds( - currencies: NonEmptyList, - ): Pair, NonEmptySet> { - val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.network - } - val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() - val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() - - requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } - requireNotNull(networks) { "Networks IDs cannot be empty" } - - return networks to currenciesIds - } - - private fun extractAddress(networkStatus: NetworkStatus?): String? { - return when (val value = networkStatus?.value) { - is NetworkStatus.NoAccount -> value.address.defaultAddress.value - is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value - is NetworkStatus.Verified -> value.address.defaultAddress.value - else -> null - } - } - sealed class Error { data object EmptyCurrencies : Error() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt similarity index 96% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt index 6ac673ec1a..255d2f86b5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt @@ -20,14 +20,16 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* -internal class CurrenciesStatusesLceOperations( +class LceCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, -) { +) : BaseCurrenciesStatusesOperations { - fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> { + override fun getCurrenciesStatuses( + userWalletId: UserWalletId, + ): LceFlow> { return transformToCurrenciesStatuses( userWalletId = userWalletId, currenciesFlow = getWalletCurrencies(userWalletId), @@ -49,7 +51,6 @@ internal class CurrenciesStatusesLceOperations( maybeNetworkStatuses = null, maybeQuotes = null, maybeYieldBalances = null, - ) send(loadingCurrencies) } @@ -217,7 +218,7 @@ internal class CurrenciesStatusesLceOperations( } } - companion object { - private const val RETRY_QUOTES_DELAY = 2000L + private companion object { + const val RETRY_QUOTES_DELAY = 2000L } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 8f2404de68..2c6bc01fc8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -20,11 +20,11 @@ internal class TokenListOperations( fun getTokenListFlow(): Flow> { return combine( - getIsGrouped(), - getIsSortedByBalance(), + flow = getIsGrouped(), + flow2 = getIsSortedByBalance(), ) { isGrouped, isSortedByBalance -> either { - createTokenList(isGrouped.bind(), isSortedByBalance.bind()) + createTokenList(isGrouped = isGrouped.bind(), isSortedByBalance = isSortedByBalance.bind()) } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt new file mode 100644 index 0000000000..bf5e006fb0 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -0,0 +1,117 @@ +package com.tangem.domain.tokens.utils + +import arrow.core.Either +import arrow.core.NonEmptyList +import arrow.core.raise.either +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error +import com.tangem.domain.tokens.operations.CurrencyStatusOperations + +/** + * Proxy creator of [CryptoCurrencyStatus]. Used [CurrencyStatusOperations] to create statuses. + * + * @property stakingRepository staking repository + * +[REDACTED_AUTHOR] + */ +class CurrencyStatusProxyCreator( + private val stakingRepository: StakingRepository, +) { + + fun createCurrencyStatus( + currency: CryptoCurrency, + maybeQuote: Either, + maybeNetworkStatus: Either, + maybeYieldBalance: Either?, + ): Either = either { + var quoteRetrievingFailed = false + + val networkStatus = maybeNetworkStatus.bind() + val quote = arrow.core.raise.recover({ maybeQuote.bind() }) { + quoteRetrievingFailed = true + null + } + val yieldBalance = maybeYieldBalance?.getOrNull() + + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = quoteRetrievingFailed, + yieldBalance = yieldBalance, + ) + } + + fun createCurrenciesStatuses( + currencies: NonEmptyList, + maybeQuotes: Either>?, + maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, + ): Either> = either { + var quotesRetrievingFailed = false + + val networksStatuses = maybeNetworkStatuses?.bind()?.toNonEmptySetOrNull() + val quotes: Set? = maybeQuotes?.fold( + ifLeft = { + quotesRetrievingFailed = true + null + }, + ifRight = { + it.ifEmpty { + quotesRetrievingFailed = true + null + } + }, + ) + + val yieldBalances = maybeYieldBalances?.getOrNull() + + currencies.map { currency -> + val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } + val address = extractAddress(networkStatus) + + val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) + val yieldBalance = if (supportedIntegration.isNullOrEmpty().not()) { + (yieldBalances as? YieldBalanceList.Data)?.getBalance( + address = address, + integrationId = supportedIntegration, + ) + } else { + null + } + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = quotesRetrievingFailed, + yieldBalance = yieldBalance, + ) + } + } + + fun createCurrencyStatus( + currency: CryptoCurrency, + quote: Quote?, + networkStatus: NetworkStatus?, + ignoreQuote: Boolean, + yieldBalance: YieldBalance?, + ): CryptoCurrencyStatus { + val currencyStatusOperations = CurrencyStatusOperations( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = ignoreQuote, + yieldBalance = yieldBalance, + ) + + return currencyStatusOperations.createTokenStatus() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/NetworkAddressExt.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/NetworkAddressExt.kt new file mode 100644 index 0000000000..e0281df74f --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/NetworkAddressExt.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.tokens.utils + +import com.tangem.domain.tokens.model.NetworkStatus + +/** Extract address from [networkStatus] */ +internal fun extractAddress(networkStatus: NetworkStatus?): String? { + return when (val value = networkStatus?.value) { + is NetworkStatus.NoAccount -> value.address.defaultAddress.value + is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value + is NetworkStatus.Verified -> value.address.defaultAddress.value + else -> null + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index b96776f92b..011dcc2efb 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.mock.MockTokensStates import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository @@ -162,17 +163,19 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { quotes: Flow>> = flowOf(MockQuotes.quotes.right()), statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), ) = GetPrimaryCurrencyStatusUpdatesUseCase( - dispatchers = dispatchers, - currenciesRepository = MockCurrenciesRepository( - sortTokensResult = Unit.right(), - removeCurrencyResult = removeCurrencyResult, - token = token, - tokens = flowOf(), - isGrouped = flowOf(), - isSortedByBalance = flowOf(), + currencyStatusOperations = CurrenciesStatusesOperations( + currenciesRepository = MockCurrenciesRepository( + sortTokensResult = Unit.right(), + removeCurrencyResult = removeCurrencyResult, + token = token, + tokens = flowOf(), + isGrouped = flowOf(), + isSortedByBalance = flowOf(), + ), + quotesRepository = MockQuotesRepository(quotes), + networksRepository = MockNetworksRepository(statuses), + stakingRepository = MockStakingRepository(), ), - quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(statuses), - stakingRepository = MockStakingRepository(), + dispatchers = dispatchers, ) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt index e48ddc2674..0491aa226a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.tokenlist.entity.utils import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount +import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference @@ -68,6 +69,7 @@ internal object OnrampTokenItemStateConverterFactory { -> { TokenItemState.Subtitle2State.TextContent( text = status.getFormattedCryptoAmount(includeStaking = false), + isFlickering = status.value.isFlickering(), ) } is CryptoCurrencyStatus.Loading, @@ -92,6 +94,7 @@ internal object OnrampTokenItemStateConverterFactory { TokenItemState.FiatAmountState.TextContent( text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = false), isAvailable = isAvailable, + isFlickering = status.value.isFlickering(), ) } is CryptoCurrencyStatus.Unreachable, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index 4f1c22ef3a..c55fb7a485 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.features.staking.impl.presentation.state.utils.isTronStakedBalance +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.isPositive import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList @@ -44,10 +45,10 @@ internal class SetConfirmationStateInitTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val actionType = when { isEnter -> StakingActionCommonType.Enter - isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled()) + isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartialUnstakeDisabled(prevState)) else -> when (pendingAction?.type) { StakingActionType.STAKE -> StakingActionCommonType.Enter - StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled()) + StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(isPartialUnstakeDisabled(prevState)) StakingActionType.CLAIM_REWARDS, StakingActionType.RESTAKE_REWARDS, -> StakingActionCommonType.Pending.Rewards @@ -80,7 +81,13 @@ internal class SetConfirmationStateInitTransformer( ) } - private fun isPartiallyUnstakeDisabled(): Boolean { + private fun isPartialUnstakeDisabled(state: StakingUiState): Boolean { + val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId) + val isValidatorPreferred = balanceState?.validator?.preferred == true + if (isSolana && !isValidatorPreferred) { + return true + } + val exitArgs = yieldArgs.exit ?: return false val exitAmount = exitArgs.args[Yield.Args.ArgType.AMOUNT] ?: return false val min = exitAmount.minimum ?: return false diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 52e15d5933..13135efa71 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,13 +1,9 @@ package com.tangem.feature.swap.domain.di -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.feature.swap.domain.* import com.tangem.lib.crypto.TransactionManager -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -40,19 +36,9 @@ internal class SwapDomainModule { @Provides @Singleton fun providesGetCryptoCurrencyStatusUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, - dispatchers: CoroutineDispatcherProvider, + currencyStatusOperations: BaseCurrencyStatusOperations, ): GetCryptoCurrencyStatusesSyncUseCase { - return GetCryptoCurrencyStatusesSyncUseCase( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - dispatchers = dispatchers, - ) + return GetCryptoCurrencyStatusesSyncUseCase(currencyStatusOperations) } @Provides diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 6c93ec7d9b..58486a978a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -218,12 +218,12 @@ internal class SwapNotificationsFactory( if (status is CryptoCurrencyStatus.NoAccount) { val amount = quoteModel.toTokenInfo.tokenAmount.value val amountToCreateAccount = status.amountToCreateAccount - val currencyTo = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency + val currencyTo = quoteModel.toTokenInfo.cryptoCurrencyStatus.currency if (amount < amountToCreateAccount) { add( SwapNotificationUM.Warning.NeedReserveToCreateAccount( - status.amountToCreateAccount.parseBigDecimal(currencyTo.decimals), - currencyTo.symbol, + receiveAmount = status.amountToCreateAccount.parseBigDecimal(currencyTo.decimals), + receiveToken = currencyTo.symbol, ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index bf1bfae85a..b6fb06af62 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -127,12 +127,12 @@ internal object SwapNotificationUM { ) data class NeedReserveToCreateAccount( - val amount: String, - val token: String, + val receiveAmount: String, + val receiveToken: String, ) : Warning( title = resourceReference( - id = R.string.send_notification_invalid_reserve_amount_title, - formatArgs = wrappedList("$amount $token"), + id = R.string.warning_express_notification_invalid_reserve_amount_title, + formatArgs = wrappedList("$receiveAmount $receiveToken"), ), subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index 365f441978..4a6aa779cb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -54,6 +54,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.KoinosMana, is TokenDetailsNotification.MigrationMaticToPol, is TokenDetailsNotification.TokensInBeta, + is TokenDetailsNotification.UsedOutdatedData, -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index abc78abbec..14740adf85 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -234,4 +234,11 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.beta_mode_warning_title), subtitle = resourceReference(id = R.string.beta_mode_warning_message), ) + + data object UsedOutdatedData : TokenDetailsNotification( + config = NotificationConfig( + subtitle = resourceReference(R.string.warning_some_token_balances_not_updated), + iconResId = R.drawable.ic_error_sync_24, + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index f8d51e82c0..50b4710dd8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -136,6 +136,7 @@ internal class TokenDetailsNotificationConverter( ) is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol is CryptoCurrencyWarning.TokensInBetaWarning -> TokensInBeta + is CryptoCurrencyWarning.UsedOutdatedDataWarning -> UsedOutdatedData } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index f60d8ad078..29775149aa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -20,12 +20,12 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems -import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData @@ -96,6 +96,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon config = it.config, iconTint = when (it) { is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent + is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention else -> null }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 7756255af2..5a5fa3536e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase @@ -68,17 +69,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addUsedOutdatedDataNotification( maybeTokenList: Lce, ) { - val tokenList = maybeTokenList.getOrNull(isPartialContentAccepted = false)?.flattenCurrencies().orEmpty() - - val hasOnlyCachedData = tokenList.any { - when (val value = it.value) { - is CryptoCurrencyStatus.Loaded -> value.source == StatusSource.ONLY_CACHE - is CryptoCurrencyStatus.NoAccount -> value.source == StatusSource.ONLY_CACHE - else -> false - } - } - - addIf(element = WalletNotification.UsedOutdatedData, condition = hasOnlyCachedData) + addIf( + element = WalletNotification.UsedOutdatedData, + condition = maybeTokenList.fold( + ifLoading = { + (it?.totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE + }, + ifContent = { + (it.totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE + }, + ifError = { false }, + ), + ) } private fun MutableList.addCriticalNotifications( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 9e7b87a25c..1c218be754 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -122,7 +122,7 @@ internal class WalletClickIntents @Inject constructor( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) } else {