Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-21 12:09:33 +03:00
commit 85423227c4
390 changed files with 11140 additions and 1722 deletions

View file

@ -14,15 +14,11 @@ import com.tangem.blockchain.common.Token as SdkToken
class ResponseCryptoCurrenciesFactory {
fun createCurrency(
currencyId: CryptoCurrency.ID,
response: UserTokensResponse,
scanResponse: ScanResponse,
): CryptoCurrency {
fun createCurrency(currencyId: String, response: UserTokensResponse, scanResponse: ScanResponse): CryptoCurrency {
return response.tokens
.asSequence()
.mapNotNull { createCurrency(it, scanResponse) }
.first { it.id == currencyId }
.first { it.id.value == currencyId }
}
fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List<CryptoCurrency> {

View file

@ -33,6 +33,7 @@ dependencies {
// region Others dependencies
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)

View file

@ -5,6 +5,7 @@ import com.tangem.blockchainsdk.compatibility.applyL2Compatibility
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.utils.retryOnError
@ -13,8 +14,10 @@ import com.tangem.data.markets.converters.*
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
@ -27,12 +30,15 @@ import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
@Suppress("LongParameterList")
internal class DefaultMarketsTokenRepository(
private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cacheRegistry: CacheRegistry,
private val tokenExchangesStore: RuntimeStateStore<List<TokenMarketExchangesResponse.Exchange>>,
) : MarketsTokenRepository {
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
@ -253,9 +259,13 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenExchanges(tokenId: String): List<TokenMarketExchange> {
return withContext(dispatcherProvider.io) {
val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow()
cacheRegistry.invokeOnExpire(key = "coins/$tokenId/exchanges", skipCache = false) {
val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow()
TokenMarketExchangeConverter.convertList(input = response.exchanges)
tokenExchangesStore.store(value = response.exchanges)
}
TokenMarketExchangeConverter.convertList(input = tokenExchangesStore.get().value)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.data.markets.converters
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.isSupportedInApp
import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse
import com.tangem.domain.markets.BuildConfig
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenQuotes
import com.tangem.utils.converter.Converter
@ -11,6 +13,9 @@ import java.math.BigDecimal
internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, TokenMarketInfo> {
private const val PROD_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/security_provider/"
private const val DEV_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api.dev/security_provider/"
override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo {
return with(value) {
TokenMarketInfo(
@ -23,6 +28,7 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
fullDescription = fullDescription,
insights = insights?.convert(),
metrics = metrics?.convert(),
securityData = securityData?.convert(),
links = links?.convert(),
pricePerformance = pricePerformance?.convert(),
exchangesAmount = exchangesAmount,
@ -105,6 +111,31 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
)
}
private fun TokenMarketInfoResponse.SecurityData.convert(): TokenMarketInfo.SecurityData {
return TokenMarketInfo.SecurityData(
totalSecurityScore = totalSecurityScore,
securityScoreProviderData = providerData.map { it.convert() },
)
}
private fun TokenMarketInfoResponse.ProviderData.convert(): TokenMarketInfo.SecurityScoreProvider {
return TokenMarketInfo.SecurityScoreProvider(
providerId = providerId,
providerName = providerName,
urlData = link?.convertToUrlData(),
securityScore = securityScore,
lastAuditDate = lastAuditDate,
iconUrl = "${getImageHost()}large/$providerId.png",
)
}
private fun String.convertToUrlData(): TokenMarketInfo.SecurityScoreProvider.UrlData {
return TokenMarketInfo.SecurityScoreProvider.UrlData(
fullUrl = this,
rootHost = this.extractMainDomainOrNull(),
)
}
private fun TokenMarketInfoResponse.Links.convert(): TokenMarketInfo.Links {
return TokenMarketInfo.Links(
officialLinks = officialLinks?.convert(),
@ -139,4 +170,27 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
high = high,
)
}
private fun String.extractMainDomainOrNull(): String? {
return runCatching {
val uri = Uri.parse(this)
val host = uri.host ?: return null
val parts = host.split(".")
if (parts.size >= 2) {
parts.takeLast(2).joinToString(".")
} else {
host
}
}.getOrNull()
}
private fun getImageHost(): String {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DEV_IMAGE_HOST
} else {
PROD_IMAGE_HOST
}
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.data.markets.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.markets.DefaultMarketsTokenRepository
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -25,6 +27,7 @@ internal object MarketsDataModule {
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
cacheRegistry: CacheRegistry,
): MarketsTokenRepository {
return DefaultMarketsTokenRepository(
marketsApi = marketsApi,
@ -32,6 +35,8 @@ internal object MarketsDataModule {
dispatcherProvider = dispatchers,
userWalletsStore = userWalletsStore,
analyticsEventHandler = analyticsEventHandler,
cacheRegistry = cacheRegistry,
tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()),
)
}
}

View file

@ -5,9 +5,9 @@ import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
internal class DefaultOnrampErrorResolver(
private val onrampErrorConverter: OnrampErrorConverter,
) : OnrampErrorResolver {
internal class DefaultOnrampErrorResolver : OnrampErrorResolver {
private val onrampErrorConverter = OnrampErrorConverter()
override fun resolve(throwable: Throwable): OnrampError {
return if (throwable is ApiResponseError.HttpException) {

View file

@ -1,22 +1,42 @@
package com.tangem.data.onramp
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.data.onramp.converters.StatusConverter
import com.tangem.data.onramp.converters.PaymentMethodConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultOnrampRepository(
private val onrampApi: OnrampApi,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
private val paymentMethodsStore: OnrampPaymentMethodsStore,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter()
private val countryConverter = CountryConverter(currencyConverter)
private val statusConverter = StatusConverter()
private val paymentMethodsConverter = PaymentMethodConverter()
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
@ -29,4 +49,78 @@ internal class DefaultOnrampRepository(
.getOrThrow()
.map(countryConverter::convert)
}
override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) {
onrampApi.getCountryByIp()
.getOrThrow()
.let(countryConverter::convert)
}
override suspend fun getStatus(txId: String): OnrampStatus = withContext(dispatchers.io) {
onrampApi.getStatus(txId)
.getOrThrow()
.let(statusConverter::convert)
}
override suspend fun saveDefaultCurrency(currency: OnrampCurrency) = withContext(dispatchers.io) {
appPreferencesStore.storeObject<OnrampCurrencyDTO>(
key = PreferencesKeys.ONRAMP_DEFAULT_CURRENCY,
value = currencyConverter.convertBack(currency),
)
}
override suspend fun getDefaultCurrencySync(): OnrampCurrency? = withContext(dispatchers.io) {
appPreferencesStore
.getObjectSyncOrNull<OnrampCurrencyDTO>(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY)
?.let(currencyConverter::convert)
}
override fun getDefaultCurrency(): Flow<OnrampCurrency?> {
return appPreferencesStore
.getObject<OnrampCurrencyDTO>(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY)
.map { it?.let(currencyConverter::convert) }
}
override suspend fun saveDefaultCountry(country: OnrampCountry) = withContext(dispatchers.io) {
appPreferencesStore.storeObject<OnrampCountryDTO>(
key = PreferencesKeys.ONRAMP_DEFAULT_COUNTRY,
value = countryConverter.convertBack(country),
)
}
override suspend fun getDefaultCountrySync(): OnrampCountry? = withContext(dispatchers.io) {
appPreferencesStore
.getObjectSyncOrNull<OnrampCountryDTO>(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY)
?.let(countryConverter::convert)
}
override fun getDefaultCountry(): Flow<OnrampCountry?> {
return appPreferencesStore
.getObject<OnrampCountryDTO>(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY)
.map { it?.let(countryConverter::convert) }
}
override suspend fun fetchPaymentMethodsIfAbsent() {
if (paymentMethodsStore.contains(PAYMENT_METHODS_KEY)) return
val response = safeApiCall(
call = { onrampApi.getPaymentMethods().bind() },
onError = {
Timber.w(it, "Unable to fetch onramp payment methods")
throw it
},
)
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response)
}
override suspend fun getPaymentMethods(): List<OnrampPaymentMethod> {
val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) {
"Onramp payment methods is absent in storage"
}
return paymentMethodsConverter.convertList(paymentMethods)
}
private companion object {
const val PAYMENT_METHODS_KEY = "onramp_payment_methods"
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.data.onramp
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSet
import com.tangem.datasource.local.preferences.utils.getObjectSetSync
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
internal class DefaultOnrampTransactionRepository(
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : OnrampTransactionRepository {
override suspend fun storeTransaction(transaction: OnrampTransaction) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->
val stored = mutablePreferences.getObjectSet<OnrampTransaction>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)
val updated = stored?.toMutableSet()
?.addOrReplace(transaction) { it.txId == transaction.txId }
?: mutableSetOf(transaction)
mutablePreferences.setObjectSet(
key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
value = updated,
)
}
}
}
override suspend fun getTransactionById(txId: String): OnrampTransaction? = withContext(dispatchers.io) {
val stored = appPreferencesStore.getObjectSetSync<OnrampTransaction>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)
stored.firstOrNull { it.txId == txId }
}
override fun getTransactions(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<OnrampTransaction>> = appPreferencesStore
.getObjectSet<OnrampTransaction>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
.map { transactions ->
transactions.filter {
it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value
}
}
override suspend fun removeTransaction(txId: String) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->
runCatching {
val stored = mutablePreferences.getObjectSet<OnrampTransaction>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)?.toMutableSet()
stored?.removeIf { it.txId == txId }
mutablePreferences.setObjectSet(
key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
value = stored ?: emptySet(),
)
}
}
}
}
}

View file

@ -2,13 +2,14 @@ package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
internal class CountryConverter : Converter<OnrampCountryDTO, OnrampCountry> {
private val currencyConverter = CurrencyConverter()
internal class CountryConverter(
private val currencyConverter: CurrencyConverter,
) : TwoWayConverter<OnrampCountryDTO, OnrampCountry> {
override fun convert(value: OnrampCountryDTO) = OnrampCountry(
id = "${value.alpha3}-${value.name}",
name = value.name,
code = value.code,
image = value.image,
@ -17,4 +18,14 @@ internal class CountryConverter : Converter<OnrampCountryDTO, OnrampCountry> {
defaultCurrency = currencyConverter.convert(value.defaultCurrency),
onrampAvailable = value.onrampAvailable,
)
override fun convertBack(value: OnrampCountry): OnrampCountryDTO = OnrampCountryDTO(
name = value.name,
code = value.code,
image = value.image,
alpha3 = value.alpha3,
continent = value.continent,
defaultCurrency = currencyConverter.convertBack(value.defaultCurrency),
onrampAvailable = value.onrampAvailable,
)
}

View file

@ -2,9 +2,9 @@ package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
internal class CurrencyConverter : Converter<OnrampCurrencyDTO, OnrampCurrency> {
internal class CurrencyConverter : TwoWayConverter<OnrampCurrencyDTO, OnrampCurrency> {
override fun convert(value: OnrampCurrencyDTO) = OnrampCurrency(
name = value.name,
@ -12,4 +12,11 @@ internal class CurrencyConverter : Converter<OnrampCurrencyDTO, OnrampCurrency>
image = value.image,
precision = value.precision,
)
override fun convertBack(value: OnrampCurrency): OnrampCurrencyDTO = OnrampCurrencyDTO(
name = value.name,
code = value.code,
image = value.image,
precision = value.precision,
)
}

View file

@ -0,0 +1,13 @@
package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.utils.converter.Converter
internal class PaymentMethodConverter : Converter<PaymentMethodDTO, OnrampPaymentMethod> {
override fun convert(value: PaymentMethodDTO): OnrampPaymentMethod = OnrampPaymentMethod(
id = value.id,
name = value.name,
imageUrl = value.image,
)
}

View file

@ -0,0 +1,30 @@
package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.utils.converter.Converter
internal class StatusConverter : Converter<OnrampStatusResponse, OnrampStatus> {
override fun convert(value: OnrampStatusResponse): OnrampStatus {
return OnrampStatus(
txId = value.txId,
providerId = value.providerId,
payoutAddress = value.payoutAddress,
status = OnrampStatus.Status.valueOf(value.status.name),
failReason = value.failReason,
externalTxId = value.externalTxId,
externalTxUrl = value.externalTxUrl,
payoutHash = value.payoutHash,
createdAt = value.createdAt,
fromCurrencyCode = value.fromCurrencyCode,
fromAmount = value.fromAmount,
toContractAddress = value.toContractAddress,
toNetwork = value.toNetwork,
toDecimals = value.toDecimals,
toAmount = value.toAmount,
toActualAmount = value.toActualAmount,
paymentMethod = value.paymentMethod,
countryCode = value.countryCode,
)
}
}

View file

@ -1,8 +1,14 @@
package com.tangem.data.onramp.di
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -16,10 +22,35 @@ internal object OnrampDataModule {
@Provides
@Singleton
fun provideOnrampRepository(onrampApi: OnrampApi, dispatchers: CoroutineDispatcherProvider): OnrampRepository {
fun provideOnrampRepository(
onrampApi: OnrampApi,
dispatchers: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
paymentMethodsStore: OnrampPaymentMethodsStore,
): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
dispatchers = dispatchers,
appPreferencesStore = appPreferencesStore,
paymentMethodsStore = paymentMethodsStore,
)
}
@Provides
@Singleton
fun provideOnrampTransactionRepository(
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): OnrampTransactionRepository {
return DefaultOnrampTransactionRepository(
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideOnrampErrorResolver(): OnrampErrorResolver {
return DefaultOnrampErrorResolver()
}
}

View file

@ -23,15 +23,6 @@ internal class DefaultPromoRepository(
}.getOrNull()
}
override suspend fun getTravalaPromoBanner(): PromoBanner? {
return runCatching(dispatchers.io) {
promoResponseConverter.convert(
tangemApi.getPromotionInfo(TRAVALA)
.getOrThrow(),
)
}.getOrNull()
}
override suspend fun getOkxPromoBanner(): PromoBanner? {
return runCatching(dispatchers.io) {
promoResponseConverter.convert(
@ -52,7 +43,6 @@ internal class DefaultPromoRepository(
private companion object {
private const val CHANGELLY_NAME = "changelly"
private const val TRAVALA = "travala"
private const val OKX = "okx"
private const val RING = "ring"
}

View file

@ -4,7 +4,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.store
@ -42,17 +41,6 @@ class DefaultPromoSettingsRepository(
)
}
override fun isReadyToShowWalletTravalaPromo(): Flow<Boolean> {
return appPreferencesStore.get(IS_WALLET_TRAVALA_PROMO_SHOWN_KEY, true)
}
override suspend fun setNeverToShowWalletTravalaPromo() {
appPreferencesStore.store(
key = IS_WALLET_TRAVALA_PROMO_SHOWN_KEY,
value = false,
)
}
override fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow<Boolean> {
return combine(
flow = appPreferencesStore

View file

@ -49,7 +49,6 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
@ -68,7 +67,6 @@ internal class DefaultStakingRepository(
private val stakingBalanceStore: StakingBalanceStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggle: StakingFeatureToggles,
private val walletManagersFacade: WalletManagersFacade,
private val getUserWalletUseCase: GetUserWalletUseCase,
moshi: Moshi,
@ -323,8 +321,6 @@ internal class DefaultStakingRepository(
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
@ -364,30 +360,26 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalance.Empty)
} else {
launch(dispatchers.io) {
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(userWalletId, address, integrationId)
.distinctUntilChanged()
.collectLatest {
if (it != null) {
send(yieldBalanceConverter.convert(it))
} else {
error("No yield balance available for currency ${cryptoCurrency.id.value}")
}
launch(dispatchers.io) {
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(userWalletId, address, integrationId)
.distinctUntilChanged()
.collectLatest {
if (it != null) {
send(yieldBalanceConverter.convert(it))
} else {
error("No yield balance available for currency ${cryptoCurrency.id.value}")
}
}
}
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
cryptoCurrency,
)
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
cryptoCurrency,
)
}
}.cancellable()
@ -395,21 +387,17 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldBalance = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalance.Empty
} else {
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId)
?: return@withContext YieldBalance.Error
val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId)
?: return@withContext YieldBalance.Error
yieldBalanceConverter.convert(result)
}
yieldBalanceConverter.convert(result)
}
override suspend fun fetchMultiYieldBalance(
@ -417,8 +405,6 @@ internal class DefaultStakingRepository(
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
@ -484,19 +470,15 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
stakingBalanceStore.get(userWalletId)
.onEach {
val balances = yieldBalanceListConverter.convert(it)
send(balances)
}
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
stakingBalanceStore.get(userWalletId)
.onEach {
val balances = yieldBalanceListConverter.convert(it)
send(balances)
}
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
}
}
@ -504,13 +486,9 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalanceList.Empty
} else {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error
yieldBalanceListConverter.convert(result)
}
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error
yieldBalanceListConverter.convert(result)
}
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {

View file

@ -18,7 +18,6 @@ import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -38,7 +37,6 @@ internal object StakingDataModule {
stakingBalanceStore: StakingBalanceStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggle: StakingFeatureToggles,
walletManagersFacade: WalletManagersFacade,
getUserWalletUseCase: GetUserWalletUseCase,
@NetworkMoshi moshi: Moshi,
@ -49,7 +47,6 @@ internal object StakingDataModule {
stakingBalanceStore = stakingBalanceStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
stakingFeatureToggle = stakingFeatureToggle,
walletManagersFacade = walletManagersFacade,
getUserWalletUseCase = getUserWalletUseCase,
moshi = moshi,

View file

@ -19,6 +19,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
/** Project - Data */
implementation(projects.core.datasource)

View file

@ -2,12 +2,11 @@ package com.tangem.data.tokens.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.*
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -26,23 +25,21 @@ internal object TokensDataModule {
@Singleton
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore,
walletManagersFacade: WalletManagersFacade,
expressAssetsStore: ExpressAssetsStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
swapServiceLoader: SwapServiceLoader,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
appPreferencesStore = appPreferencesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
expressAssetsStore = expressAssetsStore,
walletManagersFacade = walletManagersFacade,
cacheRegistry = cacheRegistry,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
swapServiceLoader = swapServiceLoader,
)
}
@ -84,15 +81,6 @@ internal object TokensDataModule {
)
}
@Provides
@Singleton
fun provideDefaultMarketCoinsRepository(
expressAssetsStore: ExpressAssetsStore,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider)
}
@Provides
@Singleton
fun provideCurrencyChecksRepository(

View file

@ -12,18 +12,14 @@ import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
@ -46,12 +42,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val expressAssetsStore: ExpressAssetsStore,
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val swapServiceLoader: SwapServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
) : CurrenciesRepository {
@ -298,23 +293,28 @@ internal class DefaultCurrenciesRepository(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency = withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrency(
currencyId = id,
response = response,
scanResponse = userWallet.scanResponse,
)
getMultiCurrencyWalletCurrency(userWalletId, id.value)
}
override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency =
withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrency(
currencyId = id,
response = response,
scanResponse = userWallet.scanResponse,
)
}
override suspend fun getNetworkCoin(
userWalletId: UserWalletId,
networkId: Network.ID,
@ -565,27 +565,7 @@ internal class DefaultCurrenciesRepository(
userWalletId: UserWalletId,
userTokens: UserTokensResponse,
) {
try {
val tokensList = userTokens.tokens
.map {
LeastTokenInfo(
contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = it.networkId,
)
}
if (tokensList.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
AssetsRequestBody(
tokensList = tokensList,
),
)
expressAssetsStore.store(userWalletId, response.getOrThrow())
}
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
swapServiceLoader.update(userWalletId, userTokens)
}
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {

View file

@ -6,13 +6,17 @@ import com.tangem.blockchain.common.MinimumSendAmountProvider
import com.tangem.blockchain.common.ReserveAmountProvider
import com.tangem.blockchain.common.UtxoAmountLimitProvider
import com.tangem.data.tokens.converters.UtxoConverter
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.CurrencyAmount
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.isZero
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@ -118,4 +122,23 @@ internal class DefaultCurrencyChecksRepository(
}
return utxoAmount?.let(UtxoConverter()::convert)
}
override suspend fun getRentInfoWarning(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
balanceAfterTransaction: BigDecimal?,
): CryptoCurrencyWarning.Rent? {
val rentData = walletManagersFacade.getRentInfo(userWalletId, currencyStatus.currency.network) ?: return null
val balanceValue = currencyStatus.value as? CryptoCurrencyStatus.Loaded ?: return null
val stakingTotalBalance =
(balanceValue.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance() ?: BigDecimal.ZERO
val amount = balanceAfterTransaction ?: balanceValue.amount
return when {
amount.isZero() && stakingTotalBalance.isZero() -> null
amount < rentData.exemptionAmount && stakingTotalBalance.isZero() -> {
CryptoCurrencyWarning.Rent(rentData.rent, rentData.exemptionAmount)
}
else -> null
}
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.data.tokens.repository
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class DefaultMarketCryptoCurrencyRepository(
private val expressAssetsStore: ExpressAssetsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MarketCryptoCurrencyRepository {
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return withContext(dispatchers.io) {
val contractAddress =
(cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
asset?.exchangeAvailable ?: false
}
}
}