Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-25 18:27:06 +05:00
parent 434382eb33
commit b54b85858f
17 changed files with 291 additions and 0 deletions

View file

@ -40,6 +40,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.nft.models)
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.yieldSupply.models)
/** Tangem libraries */
implementation(tangemDeps.blockchain)

View file

@ -144,6 +144,14 @@ interface TangemTechApi {
suspend fun activatePromoCode(@Body body: PromocodeActivationBody): ApiResponse<PromocodeActivationResponse>
// endregion
// region yield
@GET("v1/yield/markets")
suspend fun getYieldMarkets(): ApiResponse<YieldMarketsResponse>
@GET("v1/yield/token/{tokenAddress}")
suspend fun getYieldTokenStatus(@Path("tokenAddress") tokenAddress: String): ApiResponse<YieldTokenStatusResponse>
// endregion
// region account
@GET("/v1/wallets/{walletId}/accounts")
suspend fun getWalletAccounts(

View file

@ -0,0 +1,28 @@
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 = "markets") val marketDtos: List<MarketDto>,
@Json(name = "lastUpdated") val lastUpdated: String,
) {
@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 = "apy") val apy: BigDecimal,
@Json(name = "totalSupplied") val totalSupplied: String,
@Json(name = "totalBorrowed") val totalBorrowed: String,
@Json(name = "liquidityRate") val liquidityRate: String,
@Json(name = "borrowRate") val borrowRate: String,
@Json(name = "utilizationRate") val utilizationRate: BigDecimal,
@Json(name = "isActive") val isActive: Boolean,
@Json(name = "ltv") val ltv: BigDecimal,
@Json(name = "liquidationThreshold") val liquidationThreshold: BigDecimal,
)
}

View file

@ -0,0 +1,18 @@
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 YieldTokenStatusResponse(
@Json(name = "tokenAddress") val tokenAddress: String,
@Json(name = "tokenSymbol") val tokenSymbol: String,
@Json(name = "userBalance") val userBalance: String,
@Json(name = "earnedYield") val earnedYield: String,
@Json(name = "currentApy") val currentApy: BigDecimal,
@Json(name = "totalDeposited") val totalDeposited: String,
@Json(name = "moduleAddress") val moduleAddress: String,
@Json(name = "status") val status: String,
@Json(name = "lastUpdateAt") val lastUpdateAt: String,
)

View file

@ -0,0 +1,45 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object YieldSupplyModule {
@Provides
@Singleton
fun provideYieldMarketsStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): YieldMarketsStore {
return DefaultYieldMarketsStore(
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = listTypes<YieldMarketToken>(),
defaultValue = emptyList(),
),
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
)
}
}

View file

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

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.yieldsupply
import com.tangem.domain.yield.supply.models.YieldMarketToken
import kotlinx.coroutines.flow.Flow
interface YieldMarketsStore {
fun get(): Flow<List<YieldMarketToken>>
suspend fun getSyncOrNull(): List<YieldMarketToken>?
suspend fun store(items: List<YieldMarketToken>)
}

View file

@ -25,6 +25,7 @@ dependencies {
/** Domain */
implementation(projects.domain.yieldSupply)
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.walletManager)
implementation(projects.domain.legacy)

View file

@ -0,0 +1,31 @@
package com.tangem.data.yield.supply
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
internal class DefaultYieldSupplyMarketRepository(
private val techApi: TangemTechApi,
private val store: YieldMarketsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : YieldSupplyMarketRepository {
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
store.getSyncOrNull()
}
override suspend fun updateMarkets(): List<YieldMarketToken> = withContext(dispatchers.io) {
val response = techApi.getYieldMarkets().getOrThrow()
val domain = response.marketDtos.map(YieldMarketTokenConverter::convert)
store.store(domain)
domain
}
override fun getMarketsFlow(): Flow<List<YieldMarketToken>> = store.get()
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.yield.supply.converters
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.converter.Converter
internal object YieldMarketTokenConverter : Converter<YieldMarketsResponse.MarketDto, YieldMarketToken> {
override fun convert(value: YieldMarketsResponse.MarketDto): YieldMarketToken {
return YieldMarketToken(
tokenAddress = value.tokenAddress,
tokenSymbol = value.tokenSymbol,
tokenName = value.tokenName,
apy = value.apy,
totalSupplied = value.totalSupplied,
totalBorrowed = value.totalBorrowed,
liquidityRate = value.liquidityRate,
borrowRate = value.borrowRate,
utilizationRate = value.utilizationRate,
isActive = value.isActive,
ltv = value.ltv,
liquidationThreshold = value.liquidationThreshold,
)
}
}

View file

@ -1,7 +1,11 @@
package com.tangem.data.yield.supply.di
import com.tangem.data.yield.supply.DefaultYieldSupplyMarketRepository
import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -25,4 +29,18 @@ internal object YieldSupplyDataModule {
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideYieldSupplyMarketRepository(
techApi: TangemTechApi,
store: YieldMarketsStore,
dispatchers: CoroutineDispatcherProvider,
): YieldSupplyMarketRepository {
return DefaultYieldSupplyMarketRepository(
techApi = techApi,
store = store,
dispatchers = dispatchers,
)
}
}

View file

@ -15,6 +15,7 @@ tasks.withType<Test>().configureEach {
dependencies {
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.transaction)
implementation(projects.domain.legacy)

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
// region Domain modules
api(projects.domain.models)
// endregion
// region Other libraries
implementation(deps.kotlin.serialization)
}

View file

@ -0,0 +1,23 @@
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 YieldMarketToken(
val tokenAddress: String,
val tokenSymbol: String,
val tokenName: String,
val apy: SerializedBigDecimal,
val totalSupplied: String,
val totalBorrowed: String,
val liquidityRate: String,
val borrowRate: String,
val utilizationRate: SerializedBigDecimal,
val isActive: Boolean,
val ltv: SerializedBigDecimal,
val liquidationThreshold: SerializedBigDecimal,
)

View file

@ -0,0 +1,20 @@
package com.tangem.domain.yield.supply.models
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
/**
* Domain model representing a user's yield status for a specific token.
*/
@Serializable
data class YieldTokenStatus(
val tokenAddress: String,
val tokenSymbol: String,
val userBalance: String,
val earnedYield: String,
val currentApy: SerializedBigDecimal,
val totalDeposited: String,
val moduleAddress: String,
val status: String,
val lastUpdateAt: String,
)

View file

@ -0,0 +1,23 @@
package com.tangem.domain.yield.supply
import com.tangem.domain.yield.supply.models.YieldMarketToken
import kotlinx.coroutines.flow.Flow
interface YieldSupplyMarketRepository {
/**
* Get cached yield markets or null if nothing cached yet.
*/
suspend fun getCachedMarkets(): List<YieldMarketToken>?
/**
* Update markets by fetching from network and cache the result. Returns latest markets.
*/
@Throws
suspend fun updateMarkets(): List<YieldMarketToken>
/**
* Observe runtime markets updates.
*/
fun getMarketsFlow(): Flow<List<YieldMarketToken>>
}

View file

@ -354,6 +354,7 @@ include(":domain:swap:models")
include(":domain:wallet-manager")
include(":domain:wallet-manager:models")
include(":domain:yield-supply")
include(":domain:yield-supply:models")
// endregion Domain modules
// region Data modules