diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 323153c119..88c906000b 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -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) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 3b08465384..c6cc91adbf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -144,6 +144,14 @@ interface TangemTechApi { suspend fun activatePromoCode(@Body body: PromocodeActivationBody): ApiResponse // endregion + // region yield + @GET("v1/yield/markets") + suspend fun getYieldMarkets(): ApiResponse + + @GET("v1/yield/token/{tokenAddress}") + suspend fun getYieldTokenStatus(@Path("tokenAddress") tokenAddress: String): ApiResponse + // endregion + // region account @GET("/v1/wallets/{walletId}/accounts") suspend fun getWalletAccounts( 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 new file mode 100644 index 0000000000..184bb71951 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldMarketsResponse.kt @@ -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, + @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, + ) +} \ 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 new file mode 100644 index 0000000000..8303a4a388 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/YieldTokenStatusResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt new file mode 100644 index 0000000000..8b174ebbc1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt @@ -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(), + defaultValue = emptyList(), + ), + produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt new file mode 100644 index 0000000000..b282890523 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt @@ -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>, +) : YieldMarketsStore { + + override fun get(): Flow> = persistenceStore.data + + override suspend fun getSyncOrNull(): List? { + return persistenceStore.data.firstOrNull() + } + + override suspend fun store(items: List) { + persistenceStore.updateData { _ -> items } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt new file mode 100644 index 0000000000..c78c131d7a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt @@ -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> + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file diff --git a/data/yield-supply/build.gradle.kts b/data/yield-supply/build.gradle.kts index b0cc1f2844..73b4ac8ab2 100644 --- a/data/yield-supply/build.gradle.kts +++ b/data/yield-supply/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { /** Domain */ implementation(projects.domain.yieldSupply) + implementation(projects.domain.yieldSupply.models) implementation(projects.domain.walletManager) implementation(projects.domain.legacy) diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt new file mode 100644 index 0000000000..cde574a4d8 --- /dev/null +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt @@ -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? = withContext(dispatchers.io) { + store.getSyncOrNull() + } + + override suspend fun updateMarkets(): List = withContext(dispatchers.io) { + val response = techApi.getYieldMarkets().getOrThrow() + val domain = response.marketDtos.map(YieldMarketTokenConverter::convert) + store.store(domain) + domain + } + + override fun getMarketsFlow(): Flow> = store.get() +} \ 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 new file mode 100644 index 0000000000..e9483d7ae9 --- /dev/null +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/converters/YieldMarketTokenConverter.kt @@ -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 { + 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, + ) + } +} \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt index 2b2f45f06c..9afb654ca1 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/domain/yield-supply/build.gradle.kts b/domain/yield-supply/build.gradle.kts index 5648c7ee9a..bc5d9b6904 100644 --- a/domain/yield-supply/build.gradle.kts +++ b/domain/yield-supply/build.gradle.kts @@ -15,6 +15,7 @@ tasks.withType().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) diff --git a/domain/yield-supply/models/build.gradle.kts b/domain/yield-supply/models/build.gradle.kts new file mode 100644 index 0000000000..83fcc0276e --- /dev/null +++ b/domain/yield-supply/models/build.gradle.kts @@ -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) +} + 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 new file mode 100644 index 0000000000..003f1131e6 --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldMarketToken.kt @@ -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, +) \ No newline at end of file diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldTokenStatus.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldTokenStatus.kt new file mode 100644 index 0000000000..55a1da7f5c --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldTokenStatus.kt @@ -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, +) \ 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 new file mode 100644 index 0000000000..b18135a0af --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt @@ -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? + + /** + * Update markets by fetching from network and cache the result. Returns latest markets. + */ + @Throws + suspend fun updateMarkets(): List + + /** + * Observe runtime markets updates. + */ + fun getMarketsFlow(): Flow> +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index e565300fa1..ac0557883f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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