Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-03 11:52:19 +01:00
parent c7941ee1f7
commit 2b327fd3e9
32 changed files with 853 additions and 0 deletions

View file

@ -167,6 +167,7 @@ dependencies {
implementation(projects.domain.blockaid)
implementation(projects.domain.hotWallet)
implementation(projects.domain.news)
implementation(projects.domain.earn)
implementation(projects.common)
implementation(projects.common.routing)
@ -220,6 +221,7 @@ dependencies {
implementation(projects.data.yieldSupply)
implementation(projects.data.hotWallet)
implementation(projects.data.news)
implementation(projects.data.earn)
/** Features */
implementation(projects.features.referral.impl)

View file

@ -0,0 +1,48 @@
package com.tangem.tap.di.domain
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.domain.earn.usecase.*
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object EarnDomainModule {
@Provides
fun provideFetchEarnNetworksUseCase(repository: EarnRepository): FetchEarnNetworksUseCase {
return FetchEarnNetworksUseCase(repository)
}
@Provides
fun provideManageEarnNetworksUseCase(
earnRepository: EarnRepository,
userWalletsListRepository: UserWalletsListRepository,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
): GetEarnNetworksUseCase {
return GetEarnNetworksUseCase(
earnRepository = earnRepository,
userWalletsListRepository = userWalletsListRepository,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
)
}
@Provides
fun provideFetchTopEarnTokensUseCase(repository: EarnRepository): FetchTopEarnTokensUseCase {
return FetchTopEarnTokensUseCase(repository)
}
@Provides
fun provideManageTopEarnTokensUseCase(repository: EarnRepository): GetTopEarnTokensUseCase {
return GetTopEarnTokensUseCase(repository)
}
@Provides
fun provideGetEarnTokensBatchFlowUseCase(repository: EarnRepository): GetEarnTokensBatchFlowUseCase {
return GetEarnTokensBatchFlowUseCase(repository)
}
}

View file

@ -198,4 +198,18 @@ interface TangemTechApi {
*/
@POST("v1/transaction-events")
suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse<Unit>
// region Earn
@GET("v1/earn/yield/markets")
suspend fun getEarnTokens(
@Query("isForEarn") isForEarn: Boolean?,
@Query("page") page: String? = null,
@Query("limit") limit: Int? = null,
@Query("type") type: String? = null,
@Query("network") network: String? = null,
): ApiResponse<EarnListResponse>
@GET("v1/earn/networks")
suspend fun getEarnNetworks(@Query("type") type: String): ApiResponse<EarnNetworkListResponse>
// endregion
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class EarnNetworkListResponse(
@Json(name = "items") val items: List<EarnNetworkResponse>,
)
@JsonClass(generateAdapter = true)
data class EarnNetworkResponse(
@Json(name = "networkId") val networkId: String,
)

View file

@ -0,0 +1,33 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class EarnListResponse(
@Json(name = "meta") val meta: MetaEarnListResponse,
@Json(name = "items") val items: List<EarnResponse>,
)
@JsonClass(generateAdapter = true)
data class EarnResponse(
@Json(name = "apy") val apy: String,
@Json(name = "networkId") val networkId: String,
@Json(name = "rewardType") val rewardType: String,
@Json(name = "type") val type: String,
@Json(name = "token") val token: EarnTokenResponse,
)
@JsonClass(generateAdapter = true)
data class EarnTokenResponse(
@Json(name = "id") val id: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
@Json(name = "address") val address: String,
)
@JsonClass(generateAdapter = true)
data class MetaEarnListResponse(
@Json(name = "page") val page: Int,
@Json(name = "limit") val limit: Int,
)

1
data/earn/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,44 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.earn"
}
dependencies {
// region Project - Core
implementation(projects.core.datasource)
api(projects.core.utils)
// endregion
// region Project - Data
implementation(projects.data.common)
// endregion
// region Project - Domain
implementation(projects.domain.earn)
implementation(projects.domain.common)
implementation(projects.domain.account.status)
// endregion
// region Project - Libs
implementation(projects.libs.blockchainSdk)
// endregion
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Other libraries
implementation(deps.androidx.datastore)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
implementation(tangemDeps.blockchain)
// endregion
}

View file

@ -0,0 +1,20 @@
package com.tangem.data.earn
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.earn.EarnErrorResolver
import com.tangem.domain.models.earn.EarnError
internal class DefaultEarnErrorResolver : EarnErrorResolver {
override fun resolve(throwable: Throwable?): EarnError {
return when (throwable) {
is ApiResponseError.HttpException -> {
EarnError.HttpError(
code = throwable.code.numericCode,
message = throwable.message.orEmpty(),
)
}
else -> EarnError.NotHttpError
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.earn.converter
import com.tangem.datasource.api.tangemTech.models.EarnResponse
import com.tangem.domain.models.earn.EarnToken
import com.tangem.utils.converter.Converter
internal object EarnTokenConverter : Converter<EarnResponse, EarnToken> {
override fun convert(value: EarnResponse): EarnToken {
return EarnToken(
apy = value.apy,
networkId = value.networkId,
rewardType = value.rewardType,
type = value.type,
tokenId = value.token.id,
tokenSymbol = value.token.symbol,
tokenName = value.token.name,
tokenAddress = value.token.address,
)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.data.earn.datastore
import arrow.core.Either
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.domain.models.earn.EarnNetworks
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EarnNetworksStore @Inject constructor() :
RuntimeStateStore<EarnNetworks> by RuntimeStateStore(
defaultValue = Either.Right(emptyList()),
)

View file

@ -0,0 +1,13 @@
package com.tangem.data.earn.datastore
import arrow.core.Either
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.domain.models.earn.EarnTopToken
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EarnTopTokensStore @Inject constructor() :
RuntimeStateStore<EarnTopToken> by RuntimeStateStore(
defaultValue = Either.Right(emptyList()),
)

View file

@ -0,0 +1,47 @@
package com.tangem.data.earn.di
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.earn.DefaultEarnErrorResolver
import com.tangem.data.earn.repository.DefaultEarnRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.data.earn.datastore.EarnNetworksStore
import com.tangem.data.earn.datastore.EarnTopTokensStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.EarnErrorResolver
import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 EarnDataModule {
@Provides
fun provideEarnErrorResolver(): EarnErrorResolver = DefaultEarnErrorResolver()
@Provides
@Singleton
fun provideEarnRepository(
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
userWalletsListRepository: UserWalletsListRepository,
cryptoCurrencyFactory: CryptoCurrencyFactory,
earnNetworksStore: EarnNetworksStore,
earnTopTokensStore: EarnTopTokensStore,
earnErrorResolver: EarnErrorResolver,
): EarnRepository {
return DefaultEarnRepository(
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
userWalletsListRepository = userWalletsListRepository,
cryptoCurrencyFactory = cryptoCurrencyFactory,
earnNetworksStore = earnNetworksStore,
earnTopTokensStore = earnTopTokensStore,
earnErrorResolver = earnErrorResolver,
)
}
}

View file

@ -0,0 +1,157 @@
package com.tangem.data.earn.repository
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.earn.converter.EarnTokenConverter
import com.tangem.data.earn.datastore.EarnNetworksStore
import com.tangem.data.earn.datastore.EarnTopTokensStore
import com.tangem.data.earn.repository.batch.EarnTokensBatchFetcher
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.EarnResponse
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.EarnErrorResolver
import com.tangem.domain.earn.model.EarnTokensBatchFlow
import com.tangem.domain.earn.model.EarnTokensBatchingContext
import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.earn.EarnNetwork
import com.tangem.domain.models.earn.EarnNetworks
import com.tangem.domain.models.earn.EarnTokenWithCurrency
import com.tangem.domain.models.earn.EarnTopToken
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.Flow
@Suppress("LongParameterList")
internal class DefaultEarnRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
private val userWalletsListRepository: UserWalletsListRepository,
private val cryptoCurrencyFactory: CryptoCurrencyFactory,
private val earnNetworksStore: EarnNetworksStore,
private val earnTopTokensStore: EarnTopTokensStore,
private val earnErrorResolver: EarnErrorResolver,
) : EarnRepository {
override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow {
val batchFetcher = EarnTokensBatchFetcher(
tangemTechApi = tangemTechApi,
batchSize = batchSize,
userWalletsListRepository = userWalletsListRepository,
cryptoCurrencyFactory = cryptoCurrencyFactory,
)
return BatchListSource(
fetchDispatcher = dispatchers.io,
context = context,
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: INITIAL_BATCH_KEY },
batchFetcher = batchFetcher,
).toBatchFlow()
}
override suspend fun fetchEarnNetworks(type: String) {
runCatching(dispatchers.io) {
val response = tangemTechApi.getEarnNetworks(type = type).getOrThrow()
val items = response.items.map { dto ->
EarnNetwork(
networkId = dto.networkId,
isAdded = false,
)
}
earnNetworksStore.store(Either.Right(items))
}.onFailure { error ->
val earnError = earnErrorResolver.resolve(error)
earnNetworksStore.store(Either.Left(earnError))
}
}
override fun observeEarnNetworks(): Flow<EarnNetworks> {
return earnNetworksStore.get()
}
override suspend fun fetchTopEarnTokens(limit: Int) {
runCatching(dispatchers.io) {
val response = tangemTechApi.getEarnTokens(
isForEarn = true,
limit = limit,
).getOrThrow()
val userWallet = userWalletsListRepository
.selectedUserWallet
.value
.takeIf { wallet -> wallet?.isLocked?.not() == true }
?: return@runCatching
val items = response.items.mapNotNull { dto ->
val earnToken = EarnTokenConverter.convert(dto)
createCryptoCurrencyForEarnToken(
cryptoCurrencyFactory = cryptoCurrencyFactory,
userWallet = userWallet,
earnToken = dto,
)?.let { cryptoCurrency ->
EarnTokenWithCurrency(
earnToken = earnToken,
cryptoCurrency = cryptoCurrency,
)
}
}
earnTopTokensStore.store(Either.Right(items))
}.onFailure { error ->
val earnError = earnErrorResolver.resolve(error)
earnTopTokensStore.store(Either.Left(earnError))
}
}
override fun observeTopEarnTokens(): Flow<EarnTopToken> {
return earnTopTokensStore.get()
}
companion object {
internal const val FIRST_PAGE = 1
private const val INITIAL_BATCH_KEY = 0
}
}
/*
use this CryptoCurrency only for creating cryptoCurrencyIcon!! because accountIndex = DerivationIndex.Main and it
doesn't provide real all data.
*/
internal fun createCryptoCurrencyForEarnToken(
userWallet: UserWallet,
earnToken: EarnResponse,
cryptoCurrencyFactory: CryptoCurrencyFactory,
): CryptoCurrency? {
val blockchain = Blockchain.fromNetworkId(earnToken.networkId) ?: Blockchain.Unknown
return if (earnToken.token.address.isEmpty()) {
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
)
} else {
val network = cryptoCurrencyFactory.networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
accountIndex = DerivationIndex.Main,
) ?: return null
cryptoCurrencyFactory.createToken(
network = network,
rawId = CryptoCurrency.RawID(earnToken.token.id),
name = earnToken.token.name,
symbol = earnToken.token.symbol,
decimals = blockchain.decimals(),
contractAddress = earnToken.token.address,
)
}
}

View file

@ -0,0 +1,139 @@
package com.tangem.data.earn.repository.batch
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.earn.converter.EarnTokenConverter
import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE
import com.tangem.data.earn.repository.createCryptoCurrencyForEarnToken
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.EarnResponse
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.model.EarnTokensListConfig
import com.tangem.domain.models.earn.EarnTokenWithCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.exception.EndOfPaginationException
import com.tangem.pagination.fetcher.BatchFetcher
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.flow.MutableStateFlow
internal class EarnTokensBatchFetcher(
private val tangemTechApi: TangemTechApi,
private val batchSize: Int,
private val userWalletsListRepository: UserWalletsListRepository,
private val cryptoCurrencyFactory: CryptoCurrencyFactory,
) : BatchFetcher<EarnTokensListConfig, List<EarnTokenWithCurrency>> {
private val state: MutableStateFlow<EarnTokensPaginationState?> = MutableStateFlow(null)
override suspend fun fetchFirst(
requestParams: EarnTokensListConfig,
): BatchFetchResult<List<EarnTokenWithCurrency>> {
return runSuspendCatching {
loadPage(
page = FIRST_PAGE,
params = requestParams,
limit = batchSize,
)
}.fold(
onSuccess = { result ->
state.value = result.state
result.batchResult
},
onFailure = { throwable -> BatchFetchResult.Error(throwable) },
)
}
override suspend fun fetchNext(
overrideRequestParams: EarnTokensListConfig?,
lastResult: BatchFetchResult<List<EarnTokenWithCurrency>>,
): BatchFetchResult<List<EarnTokenWithCurrency>> {
val currentState = state.value ?: return BatchFetchResult.Error(
IllegalStateException("fetchFirst must be called"),
)
if (lastResult is BatchFetchResult.Success && lastResult.last && overrideRequestParams == null) {
return BatchFetchResult.Error(EndOfPaginationException())
}
val params = overrideRequestParams ?: currentState.params
val shouldReset = overrideRequestParams != null && overrideRequestParams != currentState.params
val pageToLoad = if (shouldReset) FIRST_PAGE else currentState.nextPage
return runSuspendCatching {
loadPage(
page = pageToLoad,
params = params,
limit = batchSize,
)
}.fold(
onSuccess = { result ->
state.value = result.state
result.batchResult
},
onFailure = { throwable -> BatchFetchResult.Error(throwable) },
)
}
private suspend fun loadPage(page: Int, params: EarnTokensListConfig, limit: Int): PageLoadResult {
fun createEarnTokenWithCurrency(userWallet: UserWallet, dto: EarnResponse): EarnTokenWithCurrency? {
val earnToken = EarnTokenConverter.convert(dto)
val cryptoCurrency = createCryptoCurrencyForEarnToken(
cryptoCurrencyFactory = cryptoCurrencyFactory,
userWallet = userWallet,
earnToken = dto,
)
return cryptoCurrency?.let {
EarnTokenWithCurrency(
earnToken = earnToken,
cryptoCurrency = cryptoCurrency,
)
}
}
val response = tangemTechApi.getEarnTokens(
isForEarn = params.isForEarn,
page = page.toString(),
limit = limit,
type = params.type,
network = params.network,
).getOrThrow()
val userWallet = userWalletsListRepository.selectedUserWallet.value
val items = if (userWallet == null) {
emptyList()
} else {
response.items.mapNotNull { dto ->
createEarnTokenWithCurrency(userWallet, dto)
}
}
val isLast = items.size < limit
val batchResult = BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = isLast,
)
return PageLoadResult(
batchResult = batchResult,
state = EarnTokensPaginationState(
nextPage = response.meta.page + 1,
params = params,
),
)
}
private data class PageLoadResult(
val batchResult: BatchFetchResult.Success<List<EarnTokenWithCurrency>>,
val state: EarnTokensPaginationState,
)
private data class EarnTokensPaginationState(
val nextPage: Int,
val params: EarnTokensListConfig,
)
}

1
domain/earn/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,14 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
api(projects.domain.core)
api(projects.domain.models)
api(projects.core.pagination)
implementation(projects.domain.common)
implementation(projects.domain.networks)
implementation(deps.kotlin.serialization)
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.earn
import com.tangem.domain.models.earn.EarnError
interface EarnErrorResolver {
fun resolve(throwable: Throwable?): EarnError
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.earn.model
import kotlinx.serialization.Serializable
/**
* Config of request earn tokens.
*
* @param type of Earn (ex: yield, staking).
* @param network networkId.
* @param isForEarn flag to load mostly used tokens.
*/
@Serializable
data class EarnTokensListConfig(
val type: String,
val network: String,
val isForEarn: Boolean,
)

View file

@ -0,0 +1,9 @@
package com.tangem.domain.earn.model
import com.tangem.domain.models.earn.EarnTokenWithCurrency
import com.tangem.pagination.BatchFlow
import com.tangem.pagination.BatchingContext
typealias EarnTokensBatchingContext = BatchingContext<Int, EarnTokensListConfig, Nothing>
typealias EarnTokensBatchFlow = BatchFlow<Int, List<EarnTokenWithCurrency>, Nothing>

View file

@ -0,0 +1,26 @@
package com.tangem.domain.earn.repository
import com.tangem.domain.earn.model.EarnTokensBatchFlow
import com.tangem.domain.earn.model.EarnTokensBatchingContext
import com.tangem.domain.models.earn.EarnNetworks
import com.tangem.domain.models.earn.EarnTopToken
import kotlinx.coroutines.flow.Flow
interface EarnRepository {
fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow
/**
* Load all networks for Earn and store it in data store.
*/
suspend fun fetchEarnNetworks(type: String)
fun observeEarnNetworks(): Flow<EarnNetworks>
/**
* Fetch top N earn tokens by isForEarn = true and hold it in runtime store.
*/
suspend fun fetchTopEarnTokens(limit: Int)
fun observeTopEarnTokens(): Flow<EarnTopToken>
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.earn.usecase
import arrow.core.Either
import com.tangem.domain.earn.repository.EarnRepository
class FetchEarnNetworksUseCase(
private val repository: EarnRepository,
) {
suspend operator fun invoke(type: String): Either<Throwable, Unit> {
return Either.catch {
repository.fetchEarnNetworks(type = type)
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.domain.earn.usecase
import arrow.core.Either
import com.tangem.domain.earn.repository.EarnRepository
class FetchTopEarnTokensUseCase(
private val repository: EarnRepository,
) {
suspend operator fun invoke(limit: Int = DEFAULT_LIMIT): Either<Throwable, Unit> {
return Either.catch {
repository.fetchTopEarnTokens(
limit = limit,
)
}
}
private companion object {
private const val DEFAULT_LIMIT = 5
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.domain.earn.usecase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.domain.models.earn.EarnNetwork
import com.tangem.domain.models.earn.EarnNetworks
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
/**
* Observes earn networks with [EarnNetwork.isAdded] enriched from user's wallets
* via [multiNetworkStatusSupplier]. Single entry point for all/mine filtering.
*/
class GetEarnNetworksUseCase(
private val earnRepository: EarnRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
) {
operator fun invoke(): Flow<EarnNetworks> {
return combine(
earnRepository.observeEarnNetworks(),
observeMyNetworkIds(),
) { earn, myNetworkIds ->
earn.map { earnNetworks ->
earnNetworks.map { network ->
network.copy(isAdded = network.networkId in myNetworkIds)
}
}
}.distinctUntilChanged()
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun observeMyNetworkIds(): Flow<Set<String>> {
return userWalletsListRepository.userWallets
.map { it.orEmpty() }
.flatMapLatest { wallets ->
val activeWallets = wallets
.filterNot(UserWallet::isLocked)
.filter(UserWallet::isMultiCurrency)
if (activeWallets.isEmpty()) {
flowOf(emptySet())
} else {
val flows = activeWallets.map { wallet ->
multiNetworkStatusSupplier(
MultiNetworkStatusProducer.Params(userWalletId = wallet.walletId),
).map { statuses -> statuses.map { it.network.backendId }.toSet() }
}
combine(flows) { arrays -> arrays.flatMap { it }.toSet() }
}
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.earn.usecase
import com.tangem.domain.earn.model.EarnTokensBatchFlow
import com.tangem.domain.earn.model.EarnTokensBatchingContext
import com.tangem.domain.earn.repository.EarnRepository
/**
* Returns BatchFlow of Earn-tokens.
*/
class GetEarnTokensBatchFlowUseCase(
private val repository: EarnRepository,
) {
operator fun invoke(context: EarnTokensBatchingContext, batchSize: Int = DEFAULT_BATCH_SIZE): EarnTokensBatchFlow {
return repository.getEarnTokensBatchFlow(
context = context,
batchSize = batchSize,
)
}
private companion object {
private const val DEFAULT_BATCH_SIZE = 20
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.earn.usecase
import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.domain.models.earn.EarnTopToken
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
class GetTopEarnTokensUseCase(
private val repository: EarnRepository,
) {
operator fun invoke(): Flow<EarnTopToken> {
return repository.observeTopEarnTokens().distinctUntilChanged()
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.models.earn
sealed interface EarnError {
data class HttpError(
val code: Int,
val message: String,
) : EarnError
data object NotHttpError : EarnError
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.models.earn
import kotlinx.serialization.Serializable
/**
* Model for networks in Earn.
* @param networkId - id of network
* @param isAdded - is exists in any user's wallet
*/
@Serializable
data class EarnNetwork(
val networkId: String,
val isAdded: Boolean,
)

View file

@ -0,0 +1,5 @@
package com.tangem.domain.models.earn
import arrow.core.Either
typealias EarnNetworks = Either<EarnError, List<EarnNetwork>>

View file

@ -0,0 +1,26 @@
package com.tangem.domain.models.earn
import kotlinx.serialization.Serializable
/**
* Model of earn token.
* @param apy - percent of earning
* @param networkId - id of network
* @param rewardType - apr or apy
* @param type - staking or yield
* @param tokenSymbol - symbol of token (ex. "ATOM")
* @param tokenName - name of token (ex. "Cosmos Hub")
* @param tokenId - id of token (ex. "cosmos")
* @param tokenAddress - address of contract
*/
@Serializable
data class EarnToken(
val apy: String,
val networkId: String,
val rewardType: String,
val type: String,
val tokenId: String,
val tokenSymbol: String,
val tokenName: String,
val tokenAddress: String,
)

View file

@ -0,0 +1,16 @@
package com.tangem.domain.models.earn
import com.tangem.domain.models.currency.CryptoCurrency
import kotlinx.serialization.Serializable
/**
* Represents an earn token and its associated cryptocurrency. This model is only for main account.
*
* @property earnToken The earn token details.
* @property cryptoCurrency Use this [CryptoCurrency] only for creating the cryptoCurrencyIcon!!!!
*/
@Serializable
data class EarnTokenWithCurrency(
val earnToken: EarnToken,
val cryptoCurrency: CryptoCurrency,
)

View file

@ -0,0 +1,5 @@
package com.tangem.domain.models.earn
import arrow.core.Either
typealias EarnTopToken = Either<EarnError, List<EarnTokenWithCurrency>>

View file

@ -370,6 +370,7 @@ include(":domain:wallet-manager:models")
include(":domain:yield-supply")
include(":domain:yield-supply:models")
include(":domain:news")
include(":domain:earn")
// endregion Domain modules
// region Data modules
@ -406,4 +407,5 @@ include(":data:express")
include(":data:wallet-manager")
include(":data:yield-supply")
include(":data:news")
include(":data:earn")
// endregion Data modules