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.blockaid)
|
||||||
implementation(projects.domain.hotWallet)
|
implementation(projects.domain.hotWallet)
|
||||||
implementation(projects.domain.news)
|
implementation(projects.domain.news)
|
||||||
|
implementation(projects.domain.earn)
|
||||||
|
|
||||||
implementation(projects.common)
|
implementation(projects.common)
|
||||||
implementation(projects.common.routing)
|
implementation(projects.common.routing)
|
||||||
|
|
@ -220,6 +221,7 @@ dependencies {
|
||||||
implementation(projects.data.yieldSupply)
|
implementation(projects.data.yieldSupply)
|
||||||
implementation(projects.data.hotWallet)
|
implementation(projects.data.hotWallet)
|
||||||
implementation(projects.data.news)
|
implementation(projects.data.news)
|
||||||
|
implementation(projects.data.earn)
|
||||||
|
|
||||||
/** Features */
|
/** Features */
|
||||||
implementation(projects.features.referral.impl)
|
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")
|
@POST("v1/transaction-events")
|
||||||
suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse<Unit>
|
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")
|
||||||
include(":domain:yield-supply:models")
|
include(":domain:yield-supply:models")
|
||||||
include(":domain:news")
|
include(":domain:news")
|
||||||
|
include(":domain:earn")
|
||||||
// endregion Domain modules
|
// endregion Domain modules
|
||||||
|
|
||||||
// region Data modules
|
// region Data modules
|
||||||
|
|
@ -406,4 +407,5 @@ include(":data:express")
|
||||||
include(":data:wallet-manager")
|
include(":data:wallet-manager")
|
||||||
include(":data:yield-supply")
|
include(":data:yield-supply")
|
||||||
include(":data:news")
|
include(":data:news")
|
||||||
|
include(":data:earn")
|
||||||
// endregion Data modules
|
// endregion Data modules
|
||||||
Loading…
Add table
Add a link
Reference in a new issue