Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-09 17:48:02 +05:00
parent 2e6647fa50
commit 3bde764060
23 changed files with 65 additions and 150 deletions

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldModuleStatusResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody
import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse
import retrofit2.http.Body
import retrofit2.http.GET
@ -21,7 +21,7 @@ interface YieldSupplyApi {
suspend fun getYieldTokenStatus(
@Path("chainId") chainId: Int,
@Path("tokenAddress") tokenAddress: String,
): ApiResponse<YieldTokenStatusResponse>
): ApiResponse<YieldSupplyMarketTokenDto>
@GET("api/v1/yield/token/{chainId}/{tokenAddress}/chart")
suspend fun getYieldTokenChart(

View file

@ -2,21 +2,9 @@ package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldMarketsResponse(
@Json(name = "tokens") val marketDtos: List<MarketDto>,
@Json(name = "tokens") val marketDtos: List<YieldSupplyMarketTokenDto>,
@Json(name = "lastUpdatedAt") val lastUpdated: String,
) {
@JsonClass(generateAdapter = true)
data class MarketDto(
@Json(name = "tokenAddress") val tokenAddress: String? = null,
@Json(name = "tokenSymbol") val tokenSymbol: String? = null,
@Json(name = "tokenName") val tokenName: String? = null,
@Json(name = "apy") val apy: BigDecimal,
@Json(name = "isActive") val isActive: Boolean,
@Json(name = "chainId") val chainId: Int? = null,
)
}
)

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldTokenStatusResponse(
data class YieldSupplyMarketTokenDto(
@Json(name = "tokenAddress") val tokenAddress: String? = null,
@Json(name = "tokenSymbol") val tokenSymbol: String? = null,
@Json(name = "tokenName") val tokenName: String? = null,

View file

@ -4,7 +4,7 @@ import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
@ -34,7 +34,7 @@ object YieldSupplyModule {
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = listTypes<YieldMarketsResponse.MarketDto>(),
types = listTypes<YieldSupplyMarketTokenDto>(),
defaultValue = emptyList(),
),
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },

View file

@ -1,21 +1,21 @@
package com.tangem.datasource.local.yieldsupply
import androidx.datastore.core.DataStore
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
internal class DefaultYieldMarketsStore(
private val persistenceStore: DataStore<List<YieldMarketsResponse.MarketDto>>,
private val persistenceStore: DataStore<List<YieldSupplyMarketTokenDto>>,
) : YieldMarketsStore {
override fun get(): Flow<List<YieldMarketsResponse.MarketDto>> = persistenceStore.data
override fun get(): Flow<List<YieldSupplyMarketTokenDto>> = persistenceStore.data
override suspend fun getSyncOrNull(): List<YieldMarketsResponse.MarketDto>? {
override suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>? {
return persistenceStore.data.firstOrNull()
}
override suspend fun store(items: List<YieldMarketsResponse.MarketDto>) {
override suspend fun store(items: List<YieldSupplyMarketTokenDto>) {
persistenceStore.updateData { _ -> items }
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.datasource.local.yieldsupply
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import kotlinx.coroutines.flow.Flow
interface YieldMarketsStore {
fun get(): Flow<List<YieldMarketsResponse.MarketDto>>
fun get(): Flow<List<YieldSupplyMarketTokenDto>>
suspend fun getSyncOrNull(): List<YieldMarketsResponse.MarketDto>?
suspend fun getSyncOrNull(): List<YieldSupplyMarketTokenDto>?
suspend fun store(items: List<YieldMarketsResponse.MarketDto>)
suspend fun store(items: List<YieldSupplyMarketTokenDto>)
}

View file

@ -9,7 +9,6 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.data.yield.supply.converters.YieldTokenStatusConverter
import com.tangem.data.yield.supply.converters.YieldTokenChartConverter
import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody
import com.tangem.domain.models.currency.CryptoCurrency
@ -17,7 +16,6 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -50,11 +48,11 @@ internal class DefaultYieldSupplyRepository(
it.map(YieldMarketTokenConverter::convert).enrichNetworkIds()
}
override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus {
override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketToken {
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
?: error("Chain id is required for evm's")
val response = yieldSupplyApi.getYieldTokenStatus(chainId, cryptoCurrencyToken.contractAddress).getOrThrow()
return YieldTokenStatusConverter.convert(response)
return YieldMarketTokenConverter.convert(response)
}
override suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData {

View file

@ -1,16 +1,19 @@
package com.tangem.data.yield.supply.converters
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
internal object YieldMarketTokenConverter : Converter<YieldMarketsResponse.MarketDto, YieldMarketToken> {
override fun convert(value: YieldMarketsResponse.MarketDto): YieldMarketToken {
internal object YieldMarketTokenConverter : Converter<YieldSupplyMarketTokenDto, YieldMarketToken> {
override fun convert(value: YieldSupplyMarketTokenDto): YieldMarketToken {
return YieldMarketToken(
tokenAddress = value.tokenAddress.orEmpty(),
apy = value.apy,
isActive = value.isActive,
apy = value.apy.orZero(),
isActive = value.isActive ?: false,
chainId = value.chainId ?: -1,
maxFeeUSD = value.maxFeeUSD.orEmpty(),
maxFeeNative = value.maxFeeNative.orEmpty(),
)
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.data.yield.supply.converters
import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.utils.converter.Converter
internal object YieldTokenStatusConverter : Converter<YieldTokenStatusResponse, YieldMarketTokenStatus> {
override fun convert(value: YieldTokenStatusResponse): YieldMarketTokenStatus {
return YieldMarketTokenStatus(
tokenAddress = value.tokenAddress.orEmpty(),
tokenSymbol = value.tokenSymbol.orEmpty(),
tokenName = value.tokenName.orEmpty(),
apy = value.apy ?: SerializedBigDecimal.ZERO,
isActive = value.isActive ?: false,
chainId = value.chainId ?: -1,
maxFeeUSD = value.maxFeeUSD.orEmpty(),
maxFeeNative = value.maxFeeNative.orEmpty(),
)
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.models.currency
fun CryptoCurrency.Token.yieldSupplyKey(): String {
return "${network.backendId}_$contractAddress"
}

View file

@ -12,6 +12,8 @@ data class YieldMarketToken(
val chainId: Int,
val apy: SerializedBigDecimal,
val isActive: Boolean,
val maxFeeNative: String,
val maxFeeUSD: String,
val backendId: String? = null,
) {

View file

@ -1,19 +0,0 @@
package com.tangem.domain.yield.supply.models
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
/**
* Domain model representing a token entry in the Yield Markets list.
*/
@Serializable
data class YieldMarketTokenStatus(
val tokenAddress: String,
val tokenSymbol: String,
val tokenName: String,
val chainId: Int,
val apy: SerializedBigDecimal,
val isActive: Boolean,
val maxFeeNative: String,
val maxFeeUSD: String,
)

View file

@ -3,7 +3,6 @@ package com.tangem.domain.yield.supply
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import kotlinx.coroutines.flow.Flow
@ -26,10 +25,10 @@ interface YieldSupplyRepository {
fun getMarketsFlow(): Flow<List<YieldMarketToken>>
/**
* Get yield token status by contract address.
* Get yield token status by contract address from cache
*/
@Throws
suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus
suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketToken
/**
* Get yield token APY chart by contract address.

View file

@ -8,7 +8,8 @@ class YieldSupplyActivateUseCase(
private val yieldSupplyRepository: YieldSupplyRepository,
) {
suspend operator fun invoke(cryptoCurrencyToken: CryptoCurrency.Token): Either<Throwable, Boolean> = Either.catch {
yieldSupplyRepository.activateProtocol(cryptoCurrencyToken)
suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either<Throwable, Boolean> = Either.catch {
val token = cryptoCurrency as? CryptoCurrency.Token ?: error("Token expected")
yieldSupplyRepository.activateProtocol(token)
}
}

View file

@ -8,7 +8,8 @@ class YieldSupplyDeactivateUseCase(
private val yieldSupplyRepository: YieldSupplyRepository,
) {
suspend operator fun invoke(cryptoCurrencyToken: CryptoCurrency.Token): Either<Throwable, Boolean> = Either.catch {
yieldSupplyRepository.deactivateProtocol(cryptoCurrencyToken)
suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either<Throwable, Boolean> = Either.catch {
val token = cryptoCurrency as? CryptoCurrency.Token ?: error("Token expected")
yieldSupplyRepository.deactivateProtocol(token)
}
}

View file

@ -2,14 +2,17 @@ package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.domain.yield.supply.models.YieldMarketToken
class YieldSupplyGetTokenStatusUseCase(
private val yieldSupplyRepository: YieldSupplyRepository,
) {
suspend operator fun invoke(token: CryptoCurrency.Token): Either<Throwable, YieldMarketTokenStatus> = Either.catch {
yieldSupplyRepository.getTokenStatus(token)
suspend operator fun invoke(token: CryptoCurrency.Token): Either<Throwable, YieldMarketToken> = Either.catch {
val tokens = yieldSupplyRepository.getCachedMarkets().orEmpty()
val cachedStatus = tokens.firstOrNull { it.yieldSupplyKey == token.yieldSupplyKey() }
cachedStatus ?: error("YieldMarketToken not found")
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup
import com.tangem.domain.models.wallet.UserWallet
@ -208,12 +209,9 @@ internal class TokenListStateConverter(
?.yieldSupplyStatus?.isActive == true
if (isYieldSupplyActive) return null
val contract = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)
?.contractAddress
?.lowercase() ?: return null
val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return null
val yieldSupplyKey = "${cryptoCurrencyStatus.currency.network.backendId}_$contract"
return apyMap[yieldSupplyKey]
return apyMap[token.yieldSupplyKey()]
}
private fun getOrganizeTokensButtonState(tokenList: TokenList): WalletOrganizeTokensButtonConfig? {

View file

@ -1,6 +0,0 @@
package com.tangem.features.yield.supply.impl.main.entity
internal enum class LoadingStatusMode {
Initial,
LoadApy,
}

View file

@ -22,8 +22,6 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusFailureTransformer
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
@ -118,7 +116,7 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun loadTokenStatus(mode: LoadingStatusMode) {
private fun loadTokenStatus() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
@ -127,11 +125,10 @@ internal class YieldSupplyModel @Inject constructor(
YieldSupplyTokenStatusSuccessTransformer(
tokenStatus = tokenStatus,
onStartEarningClick = ::onStartEarningClick,
mode = mode,
),
)
}.onLeft {
uiState.update(YieldSupplyTokenStatusFailureTransformer(mode))
uiState.update { YieldSupplyUM.Unavailable }
}
}
}
@ -186,14 +183,13 @@ internal class YieldSupplyModel @Inject constructor(
onClick = ::onActiveClick,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
)
else -> YieldSupplyUM.Loading
else -> YieldSupplyUM.Initial
}
uiState.update { yieldSupplyUM }
when (yieldSupplyUM) {
is YieldSupplyUM.Loading -> loadTokenStatus(LoadingStatusMode.Initial)
is YieldSupplyUM.Content -> loadTokenStatus(LoadingStatusMode.LoadApy)
is YieldSupplyUM.Initial -> loadTokenStatus()
else -> Unit
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.features.yield.supply.impl.main.model.transformers
import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.utils.transformer.Transformer
internal class YieldSupplyTokenStatusFailureTransformer(
private val mode: LoadingStatusMode,
) : Transformer<YieldSupplyUM> {
override fun transform(prevState: YieldSupplyUM): YieldSupplyUM {
return when (mode) {
LoadingStatusMode.Initial -> YieldSupplyUM.Unavailable
LoadingStatusMode.LoadApy -> prevState // TODO apply correct UI
}
}
}

View file

@ -1,42 +1,26 @@
package com.tangem.features.yield.supply.impl.main.model.transformers
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.transformer.Transformer
internal class YieldSupplyTokenStatusSuccessTransformer(
private val tokenStatus: YieldMarketTokenStatus,
private val tokenStatus: YieldMarketToken,
private val onStartEarningClick: () -> Unit,
private val mode: LoadingStatusMode,
) : Transformer<YieldSupplyUM> {
override fun transform(prevState: YieldSupplyUM): YieldSupplyUM {
if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable
return when (mode) {
LoadingStatusMode.Initial -> {
YieldSupplyUM.Available(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList(tokenStatus.apy),
),
onClick = onStartEarningClick,
)
}
LoadingStatusMode.LoadApy -> {
if (prevState is YieldSupplyUM.Content) {
prevState.copy(
rewardsApy = stringReference("${tokenStatus.apy}%"),
)
} else {
prevState
}
}
}
return YieldSupplyUM.Available(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList(tokenStatus.apy),
),
onClick = onStartEarningClick,
)
}
}

View file

@ -208,7 +208,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
},
ifRight = {
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id)
yieldSupplyActivateUseCase(cryptoCurrency as CryptoCurrency.Token)
yieldSupplyActivateUseCase(cryptoCurrency)
modelScope.launch {
params.callback.onTransactionSent()
}

View file

@ -136,7 +136,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
)
},
ifRight = {
yieldSupplyDeactivateUseCase(cryptoCurrency as CryptoCurrency.Token)
yieldSupplyDeactivateUseCase(cryptoCurrency)
params.callback.onTransactionSent()
},
)