Updated on 2026-08-14
This commit is contained in:
parent
c7941ee1f7
commit
2b327fd3e9
32 changed files with 853 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
1
data/earn/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
44
data/earn/build.gradle.kts
Normal file
44
data/earn/build.gradle.kts
Normal 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
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()),
|
||||
)
|
||||
|
|
@ -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()),
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
1
domain/earn/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
14
domain/earn/build.gradle.kts
Normal file
14
domain/earn/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.earn
|
||||
|
||||
import com.tangem.domain.models.earn.EarnError
|
||||
|
||||
interface EarnErrorResolver {
|
||||
|
||||
fun resolve(throwable: Throwable?): EarnError
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.models.earn
|
||||
|
||||
import arrow.core.Either
|
||||
|
||||
typealias EarnNetworks = Either<EarnError, List<EarnNetwork>>
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.models.earn
|
||||
|
||||
import arrow.core.Either
|
||||
|
||||
typealias EarnTopToken = Either<EarnError, List<EarnTokenWithCurrency>>
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue