From fefe37a890d676d905c8506cbc4f1bf6eac00191 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 31 Aug 2023 16:58:05 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 1 + .../common/redux/global/GlobalMiddleware.kt | 2 + .../java/com/tangem/tap/di/ActivityModule.kt | 9 ++ .../tap/di/domain/TokensDomainModule.kt | 6 +- .../tokens/LoadAvailableCoinsService.kt | 105 ------------------ .../converters/CryptoCurrencyConverter.kt | 28 ++++- .../exchangeServices/DefaultRampManager.kt | 21 ++++ .../com/tangem/tap/proxy/AppStateHolder.kt | 2 + .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../datasource/di/MarketCoinsStoreModule.kt | 21 ++++ .../token/DefaultUserMarketCoinsStore.kt | 18 +++ .../local/token/UserMarketCoinsStore.kt | 11 ++ .../tangem/data/tokens/di/TokensDataModule.kt | 41 +++++-- .../repository/DefaultCurrenciesRepository.kt | 14 +++ .../DefaultMarketCryptoCurrencyRepository.kt | 18 +++ .../domain/exchange/RampStateManager.kt | 11 ++ domain/tokens/build.gradle.kts | 8 +- .../domain/tokens/models/CryptoCurrency.kt | 2 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 74 ++++++++++-- .../domain/tokens/model/TokenActionsState.kt | 3 +- .../MarketCryptoCurrencyRepository.kt | 12 ++ .../viewmodels/TokenDetailsViewModel.kt | 6 +- .../wallet/viewmodels/WalletViewModel.kt | 10 +- 23 files changed, 285 insertions(+), 140 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt create mode 100644 app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 65bbb228d0..79fee709bf 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -190,6 +190,7 @@ class TapApplication : Application(), ImageLoaderFactory { scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, + appStateHolder = appStateHolder, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 287b5d35f1..4f74fdbdf9 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -131,6 +131,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di sellService = makeSellExchangeService(config), primaryRules = CardExchangeRules(cardProvider), ) + // TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance) + store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) store.dispatchOnMain(GlobalAction.ExchangeManager.Update) } diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index a894d7c4fe..58073accdf 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -3,8 +3,11 @@ package com.tangem.tap.di import android.content.Context import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.exchange.RampStateManager import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository +import com.tangem.tap.network.exchangeServices.DefaultRampManager +import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.userTokensRepository import dagger.Module import dagger.Provides @@ -40,4 +43,10 @@ internal object ActivityModule { ), ) } + + @Provides + @Singleton + fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager { + return DefaultRampManager(appStateHolder.exchangeService) + } } \ No newline at end of file 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 1a77f50c7b..8ac2b0b3b2 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 @@ -1,7 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -108,8 +110,10 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideGetCryptoCurrencyActionsUseCase( + rampStateManager: RampStateManager, + marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { - return GetCryptoCurrencyActionsUseCase(dispatchers) + return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt deleted file mode 100644 index 39e845f7b2..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.tap.domain.tokens - -import com.squareup.moshi.JsonAdapter -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.services.Result -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.datasource.asset.AssetReader -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class LoadAvailableCoinsService( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val assetReader: AssetReader, -) { - private val currenciesAdapter: JsonAdapter = - MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java) - - suspend fun getSupportedTokens( - isTestNet: Boolean, - supportedBlockchains: List, - page: Int, - searchInput: String?, - ): Result { - if (isTestNet) { - return Result.Success( - LoadedCoins( - currencies = getTestnetCoins().filter(searchInput), - moreAvailable = false, - ), - ) - } - - val offset = page * LOAD_PER_PAGE - return when (val result = loadCoins(supportedBlockchains, offset, searchInput)) { - is Result.Success -> { - val data = result.data - - Result.Success( - LoadedCoins( - currencies = data.coins.map { - Currency.fromCoinResponse(currency = it, imageHost = data.imageHost) - }, - moreAvailable = data.total > offset + LOAD_PER_PAGE, - ), - ) - } - is Result.Failure -> { - Result.Failure(result.error) - } - } - } - - private suspend fun loadCoins( - supportedBlockchains: List, - offset: Int, - searchInput: String?, - ): Result { - return withContext(dispatchers.io) { - runCatching { - tangemTechApi.getCoins( - networkIds = supportedBlockchains.joinToString( - separator = ",", - transform = Blockchain::toNetworkId, - ), - active = true, - searchText = searchInput, - offset = offset, - limit = LOAD_PER_PAGE, - ) - }.fold( - onSuccess = { Result.Success(it) }, - onFailure = { Result.Failure(it) }, - ) - } - } - - private fun getTestnetCoins(): List { - val json = assetReader.readJson(FILE_NAME_TESTNET_COINS) - return currenciesAdapter.fromJson(json)!!.coins - .map { Currency.fromJsonObject(it) } - } - - private fun List.filter(searchInput: String?): List { - if (searchInput.isNullOrBlank()) return this - - return filter { currency -> - currency.symbol.contains(searchInput, ignoreCase = true) || - currency.name.contains(searchInput, ignoreCase = true) - } - } - - private companion object { - const val LOAD_PER_PAGE = 100 - const val FILE_NAME_TESTNET_COINS = "testnet_tokens" - } -} - -data class LoadedCoins( - val currencies: List, - val moreAvailable: Boolean, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt index 8b22dd95e6..832de0bda1 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -1,13 +1,15 @@ package com.tangem.tap.features.wallet.converters +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class CryptoCurrencyConverter : Converter { +internal class CryptoCurrencyConverter : TwoWayConverter { private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } @@ -40,4 +42,26 @@ internal class CryptoCurrencyConverter : Converter { ) } } + + override fun convertBack(value: CryptoCurrency): Currency { + val blockchain = Blockchain.fromId(value.network.id.value) + if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain") + return when (value) { + is CryptoCurrency.Coin -> Currency.Blockchain( + blockchain = blockchain, + derivationPath = value.derivationPath, + ) + is CryptoCurrency.Token -> Currency.Token( + token = Token( + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + id = value.id.value, + ), + blockchain = blockchain, + derivationPath = value.derivationPath, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt new file mode 100644 index 0000000000..9c9184e488 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter + +class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager { + + private val cryptoCurrencyConverter = CryptoCurrencyConverter() + override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForBuy( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } + + override fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForSell( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cdcbc7fd9a..5aa105d76a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -14,6 +14,7 @@ import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.network.exchangeServices.ExchangeService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import org.rekotlin.Action @@ -45,6 +46,7 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl var tangemSdkManager: TangemSdkManager? = null var walletStoresManager: WalletStoresManager? = null var appFiatCurrency: FiatCurrency = FiatCurrency.Default + var exchangeService: ExchangeService? = null fun getActualCard(): CardDTO? { return scanResponse?.card diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index c677f70300..324719823c 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -16,6 +16,7 @@ import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles +import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType data class DaggerGraphState( @@ -35,6 +36,7 @@ data class DaggerGraphState( val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, val walletManagersFacade: WalletManagersFacade? = null, + val appStateHolder: AppStateHolder? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt new file mode 100644 index 0000000000..bd2098a670 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore +import com.tangem.datasource.local.token.UserMarketCoinsStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object MarketCoinsStoreModule { + + @Provides + @Singleton + fun provideUserMarketCoinsStore(): UserMarketCoinsStore { + return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt new file mode 100644 index 0000000000..c08a627df9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.wallets.models.UserWalletId + +internal class DefaultUserMarketCoinsStore( + private val dataStore: StringKeyDataStore, +) : UserMarketCoinsStore { + + override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? { + return dataStore.getSyncOrNull(userWalletId.stringValue) + } + + override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) { + dataStore.store(userWalletId.stringValue, item) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt new file mode 100644 index 0000000000..10895699ac --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.domain.wallets.models.UserWalletId + +interface UserMarketCoinsStore { + + suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? + + suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index c1229cde5b..cd55cde081 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -2,14 +2,17 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.DefaultCurrenciesRepository +import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository import com.tangem.data.tokens.repository.DefaultNetworksRepository import com.tangem.data.tokens.repository.DefaultQuotesRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore import com.tangem.datasource.local.quote.QuotesStore +import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -30,10 +33,18 @@ internal object TokensDataModule { tangemTechApi: TangemTechApi, userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, + userMarketCoinsStore: UserMarketCoinsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): CurrenciesRepository { - return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) + return DefaultCurrenciesRepository( + tangemTechApi = tangemTechApi, + userTokensStore = userTokensStore, + userWalletsStore = userWalletsStore, + userMarketCoinsStore = userMarketCoinsStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, + ) } @Provides @@ -46,11 +57,11 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, ): QuotesRepository { return DefaultQuotesRepository( - tangemTechApi, - quotesStore, - selectedAppCurrencyStore, - cacheRegistry, - dispatchers, + tangemTechApi = tangemTechApi, + quotesStore = quotesStore, + selectedAppCurrencyStore = selectedAppCurrencyStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, ) } @@ -64,11 +75,19 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, ): NetworksRepository { return DefaultNetworksRepository( - walletManagersFacade, - userWalletsStore, - userTokensStore, - cacheRegistry, - dispatchers, + walletManagersFacade = walletManagersFacade, + userWalletsStore = userWalletsStore, + userTokensStore = userTokensStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideDefaultMarketCoinsRepository( + userMarketCoinsStore: UserMarketCoinsStore, + ): MarketCryptoCurrencyRepository { + return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 0df288c0c0..d90b0321e4 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -6,6 +6,7 @@ import com.tangem.data.tokens.utils.ResponseCurrenciesFactory import com.tangem.data.tokens.utils.UserTokensResponseFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider @@ -27,6 +28,7 @@ internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, + private val userMarketCoinsStore: UserMarketCoinsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -172,6 +174,7 @@ internal class DefaultCurrenciesRepository( val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue) userTokensStore.store(userWallet.walletId, response) + fetchUserMarketCoinsByIds(userWallet.walletId, response) } catch (e: Throwable) { handleFetchTokensErrorOrThrow(userWallet, e) } @@ -182,6 +185,17 @@ internal class DefaultCurrenciesRepository( tangemTechApi.saveUserTokens(userWalletId.stringValue, response) } + private suspend fun fetchUserMarketCoinsByIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + try { + val response = tangemTechApi.getCoins( + networkIds = userTokens.tokens.joinToString(separator = ",") { it.networkId }, + ) + userMarketCoinsStore.store(userWalletId, response) + } catch (e: Throwable) { + Timber.e("Unable to fetch user market coins for: ${userWalletId.stringValue} ${e.message}") + } + } + private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) { val errorMessage = error.message ?: throw error diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt new file mode 100644 index 0000000000..891a7c14d2 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.data.tokens.repository + +import com.tangem.datasource.local.token.UserMarketCoinsStore +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository +import com.tangem.domain.wallets.models.UserWalletId + +class DefaultMarketCryptoCurrencyRepository( + private val userMarketCoinsStore: UserMarketCoinsStore, +) : MarketCryptoCurrencyRepository { + + override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean { + return userMarketCoinsStore.getSyncOrNull(userWalletId)?.coins + ?.firstOrNull { it.id == cryptoCurrencyId.rawCurrencyId } + ?.networks + ?.firstOrNull { it.networkId == cryptoCurrencyId.rawNetworkId }?.exchangeable ?: false + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt new file mode 100644 index 0000000000..b6a2e29f06 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.exchange + +import com.tangem.domain.tokens.models.CryptoCurrency + +/** + * Manager that holds info about available actions as Sell and Buy + */ +interface RampStateManager { + fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean + fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean +} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 6fae380259..17599c040d 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -1,13 +1,19 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.tokens" +} + dependencies { /** Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.models) + implementation(projects.domain.legacy) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt index 4fc784b4a8..d20a5f6091 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -93,6 +93,8 @@ sealed class CryptoCurrency : Serializable { val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.rawId + val rawNetworkId: String = networkId.value + /** * Represents the different types of prefixes that can be associated with a cryptocurrency ID. * These prefixes can help in quickly categorizing the type of cryptocurrency. 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 d705aacf94..8f0f5499af 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 @@ -1,34 +1,84 @@ package com.tangem.domain.tokens +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +/** + * Use case to determine which TokenActions are available for a [CryptoCurrency] + * + * @property rampManager Ramp manager to check ramp availability + */ class GetCryptoCurrencyActionsUseCase( + private val rampManager: RampStateManager, + private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId, tokenId: String): Flow { + operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow { return flow { - emit(getMockState(userWalletId, tokenId)) + val actionStates = createTokenActionsState(userWalletId, cryptoCurrency) + emit(actionStates) }.flowOn(dispatchers.io) } - // TODO replace by real data - private fun getMockState(userWalletId: UserWalletId, tokenId: String): TokenActionsState { + private suspend fun createTokenActionsState( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): TokenActionsState { return TokenActionsState( walletId = userWalletId, - tokenId = tokenId, - states = listOf( - TokenActionsState.ActionState.Buy(true), - TokenActionsState.ActionState.Send(true), - TokenActionsState.ActionState.Receive(true), - TokenActionsState.ActionState.Sell(true), - TokenActionsState.ActionState.Swap(true), - ), + cryptoCurrencyId = cryptoCurrency.id, + states = createListOfActions(userWalletId, cryptoCurrency), ) } + + /** + * Creates list of action for expected order + * Actions priority: [Buy Send Receive Sell Swap] + */ + private suspend fun createListOfActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): List { + return buildList { + // todo add check available in swap 1inch etc if backend doen't handle it + if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency.id) && + !isCustomToken(cryptoCurrency) + ) { + addFirst(TokenActionsState.ActionState.Swap(true)) + } else { + add(TokenActionsState.ActionState.Swap(false)) + } + + if (rampManager.availableForSell(cryptoCurrency)) { + addFirst(TokenActionsState.ActionState.Sell(true)) + } else { + add(TokenActionsState.ActionState.Sell(false)) + } + + addFirst(TokenActionsState.ActionState.Receive(true)) + addFirst(TokenActionsState.ActionState.Send(true)) + + if (rampManager.availableForBuy(cryptoCurrency)) { + addFirst(TokenActionsState.ActionState.Buy(true)) + } else { + add(TokenActionsState.ActionState.Buy(false)) + } + } + } + + private fun isCustomToken(currency: CryptoCurrency): Boolean { + return currency is CryptoCurrency.Token && currency.isCustom + } + + private fun MutableList.addFirst(item: T) { + this.add(0, item) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index b2d6e6e71b..81c3d9a53f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -1,10 +1,11 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId data class TokenActionsState( val walletId: UserWalletId, - val tokenId: String, + val cryptoCurrencyId: CryptoCurrency.ID, val states: List, ) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt new file mode 100644 index 0000000000..a8c12b480d --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * MarketCryptoCurrencyRepository works with data from Tangem coins backend, CoinMarketCap etc + */ +interface MarketCryptoCurrencyRepository { + + suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f4defde6c4..76357b097f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -84,12 +84,12 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet) { updateMarketPrice(selectedWallet = selectedWallet) - updateButtons(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id.value) + updateButtons(userWalletId = selectedWallet.walletId, currency = cryptoCurrency) updateTxHistory() } - private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { - getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency) .distinctUntilChanged() .onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) } .flowOn(dispatchers.io) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 509377d260..c02df958f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -201,7 +201,6 @@ internal class WalletViewModel @Inject constructor( private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) { val wallet = getWallet(index) val blockchain = getCardTypeResolver(index).getBlockchain() - updateButtons(userWalletId = wallet.walletId, currencyId = blockchain.id) updateTxHistory( blockchain = blockchain, derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), @@ -244,15 +243,18 @@ internal class WalletViewModel @Inject constructor( isRefreshing = isRefreshing, ) - either.onRight { status -> cryptoCurrencyStatus = status } + either.onRight { status -> + cryptoCurrencyStatus = status + updateButtons(userWalletId = userWalletId, currency = status.currency) + } } .flowOn(dispatchers.io) .launchIn(viewModelScope) .saveIn(marketPriceJobHolder) } - private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { - getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency) .distinctUntilChanged() .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) } .flowOn(dispatchers.io)