From 173ee1312091419f370b8628795ee128351f86ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Oct 2025 11:22:18 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../api/common/config/YieldSupply.kt | 6 ++++ .../api/tangemTech/YieldSupplyApi.kt | 33 ++++++++++++++++--- .../tangemTech/models/YieldMarketsResponse.kt | 7 ++-- .../models/YieldModuleStatusResponse.kt | 13 ++++++++ .../YieldSupplyChangeTokenStatusBody.kt | 10 ++++++ .../models/YieldTokenStatusResponse.kt | 13 +++++--- .../config/environment/EnvironmentConfig.kt | 1 + .../converter/EnvironmentConfigConverter.kt | 1 + .../models/EnvironmentConfigModel.kt | 1 + .../managers/MockEnvironmentConfigStorage.kt | 2 ++ .../managers/ProdApiConfigsManagerTest.kt | 3 +- .../DefaultYieldSupplyMarketRepository.kt | 32 ++++++++++++++---- .../converters/YieldMarketTokenConverter.kt | 5 ++- .../converters/YieldTokenStatusConverter.kt | 22 ++++++++----- .../yield/supply/models/YieldMarketToken.kt | 10 ++++-- .../supply/models/YieldMarketTokenStatus.kt | 19 +++++++++++ .../supply/YieldSupplyMarketRepository.kt | 6 ++-- .../usecase/YieldSupplyApyFlowUseCase.kt | 4 +-- .../YieldSupplyGetTokenStatusUseCase.kt | 6 ++-- .../converter/TokenListStateConverter.kt | 3 +- .../impl/main/model/YieldSupplyModel.kt | 2 +- 21 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldModuleStatusResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldSupplyChangeTokenStatusBody.kt create mode 100644 domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketTokenStatus.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index 774d36cf67..9fee6a798a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -63,6 +63,12 @@ internal class YieldSupply( ) private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap { + put( + key = "Authorization", + value = ProviderSuspend { + "Bearer " + environmentConfigStorage.getConfigSync().yieldModuleApiKey + }, + ) put(key = "api-key", value = ProviderSuspend { getApiKey(apiEnvironment) }) putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values) putAll(from = RequestHeader.AuthenticationHeader(authProvider).values) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt index b8db77d439..20aae40e90 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt @@ -2,19 +2,42 @@ package com.tangem.datasource.api.tangemTech 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.YieldTokenChartResponse +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query interface YieldSupplyApi { @GET("api/v1/yield/markets") - suspend fun getYieldMarkets(): ApiResponse + suspend fun getYieldMarkets(@Query("chainId") chainId: Int? = null): ApiResponse - @GET("api/v1/yield/token/{tokenAddress}") - suspend fun getYieldTokenStatus(@Path("tokenAddress") tokenAddress: String): ApiResponse + @GET("api/v1/yield/token/{chainId}/{tokenAddress}") + suspend fun getYieldTokenStatus( + @Path("chainId") chainId: Int, + @Path("tokenAddress") tokenAddress: String, + ): ApiResponse - @GET("api/v1/yield/token/{tokenAddress}/chart") - suspend fun getYieldTokenChart(@Path("tokenAddress") tokenAddress: String): ApiResponse + @GET("api/v1/yield/token/{chainId}/{tokenAddress}/chart") + suspend fun getYieldTokenChart( + @Path("chainId") chainId: Int, + @Path("tokenAddress") tokenAddress: String, + @Query("window") window: String? = null, + @Query("bucketSizeDays") bucketSizeDays: Int? = null, + ): ApiResponse + + @POST("api/v1/module/activate") + suspend fun activateYieldModule( + @Body body: YieldSupplyChangeTokenStatusBody, + ): ApiResponse + + @POST("api/v1/module/deactivate") + suspend fun deactivateYieldModule( + @Body body: YieldSupplyChangeTokenStatusBody, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldMarketsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldMarketsResponse.kt index 936c4954bf..ac62a8c655 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldMarketsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldMarketsResponse.kt @@ -12,10 +12,11 @@ data class YieldMarketsResponse( @JsonClass(generateAdapter = true) data class MarketDto( - @Json(name = "tokenAddress") val tokenAddress: String, - @Json(name = "tokenSymbol") val tokenSymbol: String, - @Json(name = "tokenName") val tokenName: String, + @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, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldModuleStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldModuleStatusResponse.kt new file mode 100644 index 0000000000..b12bfa05c1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldModuleStatusResponse.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class YieldModuleStatusResponse( + @Json(name = "tokenAddress") val tokenAddress: String, + @Json(name = "chainId") val chainId: Int, + @Json(name = "isActive") val isActive: Boolean, + @Json(name = "activatedAt") val activatedAt: String?, + @Json(name = "deactivatedAt") val deactivatedAt: String?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldSupplyChangeTokenStatusBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldSupplyChangeTokenStatusBody.kt new file mode 100644 index 0000000000..68db0ea530 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldSupplyChangeTokenStatusBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class YieldSupplyChangeTokenStatusBody( + @Json(name = "tokenAddress") val tokenAddress: String, + @Json(name = "chainId") val chainId: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldTokenStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldTokenStatusResponse.kt index a0232bd3c9..6788fc5292 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldTokenStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldTokenStatusResponse.kt @@ -6,9 +6,12 @@ import java.math.BigDecimal @JsonClass(generateAdapter = true) data class YieldTokenStatusResponse( - @Json(name = "tokenAddress") val tokenAddress: String, - @Json(name = "tokenSymbol") val tokenSymbol: String, - @Json(name = "tokenName") val tokenName: String, - @Json(name = "apy") val apy: BigDecimal, - @Json(name = "isActive") val isActive: Boolean, + @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? = null, + @Json(name = "isActive") val isActive: Boolean? = null, + @Json(name = "chainId") val chainId: Int? = null, + @Json(name = "maxFeeNative") val maxFeeNative: String? = null, + @Json(name = "maxFeeUSD") val maxFeeUSD: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index a960c5b593..384af56181 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -20,4 +20,5 @@ data class EnvironmentConfig( val tangemApiKey: String? = null, val tangemApiKeyDev: String? = null, val tangemApiKeyStage: String? = null, + val yieldModuleApiKey: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt index 4fcac008f1..cf52beb064 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt @@ -29,6 +29,7 @@ internal object EnvironmentConfigConverter : Converter? = withContext(dispatchers.io) { - store.getSyncOrNull() + store.getSyncOrNull()?.enrichNetworkIds() } override suspend fun updateMarkets(): List = withContext(dispatchers.io) { @@ -30,15 +37,28 @@ internal class DefaultYieldSupplyMarketRepository( domain } - override fun getMarketsFlow(): Flow> = store.get() + override fun getMarketsFlow(): Flow> = store.get().map { + it.enrichNetworkIds() + } - override suspend fun getTokenStatus(contractAddress: String): YieldMarketToken { - val response = yieldSupplyApi.getYieldTokenStatus(contractAddress).getOrThrow() + override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus { + 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) } - override suspend fun getTokenChart(contractAddress: String): YieldSupplyMarketChartData { - val response = yieldSupplyApi.getYieldTokenChart(contractAddress).getOrThrow() + override suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData { + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + ?: error("Chain id is required for evm's") + val response = yieldSupplyApi.getYieldTokenChart(chainId, cryptoCurrencyToken.contractAddress).getOrThrow() return YieldTokenChartConverter.convert(response) } + + private fun List.enrichNetworkIds(): List { + val chainIdMap = Blockchain.entries.associate { it.getChainId() to it.toNetworkId() } + return this.map { token -> + token.copy(backendId = chainIdMap[token.chainId]) + } + } } \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldMarketTokenConverter.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldMarketTokenConverter.kt index 82ff8d2d18..ca069668ff 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldMarketTokenConverter.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldMarketTokenConverter.kt @@ -7,11 +7,10 @@ import com.tangem.utils.converter.Converter internal object YieldMarketTokenConverter : Converter { override fun convert(value: YieldMarketsResponse.MarketDto): YieldMarketToken { return YieldMarketToken( - tokenAddress = value.tokenAddress, - tokenSymbol = value.tokenSymbol, - tokenName = value.tokenName, + tokenAddress = value.tokenAddress.orEmpty(), apy = value.apy, isActive = value.isActive, + chainId = value.chainId ?: -1, ) } } \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldTokenStatusConverter.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldTokenStatusConverter.kt index fa03345c97..8035634fc9 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldTokenStatusConverter.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldTokenStatusConverter.kt @@ -1,17 +1,21 @@ package com.tangem.data.yield.supply.converters import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse -import com.tangem.domain.yield.supply.models.YieldMarketToken +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 { - override fun convert(value: YieldTokenStatusResponse): YieldMarketToken { - return YieldMarketToken( - tokenAddress = value.tokenAddress, - tokenSymbol = value.tokenSymbol, - tokenName = value.tokenName, - apy = value.apy, - isActive = value.isActive, +internal object YieldTokenStatusConverter : Converter { + 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(), ) } } \ No newline at end of file diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketToken.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketToken.kt index 0223cd6630..900bb790b2 100644 --- a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketToken.kt +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketToken.kt @@ -9,8 +9,12 @@ import kotlinx.serialization.Serializable @Serializable data class YieldMarketToken( val tokenAddress: String, - val tokenSymbol: String, - val tokenName: String, + val chainId: Int, val apy: SerializedBigDecimal, val isActive: Boolean, -) \ No newline at end of file + val backendId: String? = null, +) { + + val yieldSupplyKey: String + get() = "${backendId}_$tokenAddress" +} \ No newline at end of file diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketTokenStatus.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketTokenStatus.kt new file mode 100644 index 0000000000..d7a6eedcf3 --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketTokenStatus.kt @@ -0,0 +1,19 @@ +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, +) \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt index 8de4c5ba6a..5d26c82073 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt @@ -1,6 +1,8 @@ package com.tangem.domain.yield.supply +import com.tangem.domain.models.currency.CryptoCurrency 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,11 +28,11 @@ interface YieldSupplyMarketRepository { * Get yield token status by contract address. */ @Throws - suspend fun getTokenStatus(contractAddress: String): YieldMarketToken + suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus /** * Get yield token APY chart by contract address. */ @Throws - suspend fun getTokenChart(contractAddress: String): YieldSupplyMarketChartData + suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt index 55ad0d7e95..9efcab1ea6 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt @@ -18,8 +18,8 @@ class YieldSupplyApyFlowUseCase( operator fun invoke(): Flow> { return yieldSupplyMarketRepository.getMarketsFlow() .map { yieldMarketTokenList -> - yieldMarketTokenList.filter { it.isActive }.associate { - it.tokenAddress to it.apy.toString() + yieldMarketTokenList.filter { it.isActive }.associate { token -> + token.yieldSupplyKey to token.apy.toString() } } } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt index b696f2429c..3a6d689924 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt @@ -3,13 +3,13 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.yield.supply.YieldSupplyMarketRepository -import com.tangem.domain.yield.supply.models.YieldMarketToken +import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus class YieldSupplyGetTokenStatusUseCase( private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, ) { - suspend operator fun invoke(token: CryptoCurrency.Token): Either = Either.catch { - yieldSupplyMarketRepository.getTokenStatus(token.contractAddress) + suspend operator fun invoke(token: CryptoCurrency.Token): Either = Either.catch { + yieldSupplyMarketRepository.getTokenStatus(token) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 46dcefb0d7..931921c425 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -213,7 +213,8 @@ internal class TokenListStateConverter( ?.contractAddress ?.lowercase() ?: return null - return apyMap[contract] + val yieldSupplyKey = "${cryptoCurrencyStatus.currency.network.backendId}_$contract" + return apyMap[yieldSupplyKey] } private fun getOrganizeTokensButtonState(tokenList: TokenList): WalletOrganizeTokensButtonConfig? { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 3b196b4cfa..d4c2c34325 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -110,7 +110,7 @@ internal class YieldSupplyModel @Inject constructor( YieldSupplyUM.Initial( title = resourceReference( id = R.string.yield_module_token_details_earn_notification_title, - formatArgs = wrappedList("5.1"), + formatArgs = wrappedList(tokenStatus.apy), ), onClick = ::onStartEarningClick, )