Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-01 16:55:53 +05:00
parent 59a6a4cb69
commit 8d6992ad24
14 changed files with 251 additions and 82 deletions

View file

@ -4,6 +4,7 @@ import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.domain.yield.supply.usecase.*
import dagger.Module
@ -77,4 +78,14 @@ internal object YieldSupplyDomainModule {
yieldSupplyErrorResolver = yieldSupplyErrorResolver,
)
}
@Provides
@Singleton
fun provideYieldSupplyGetTokenStatusUseCase(
yieldSupplyMarketRepository: YieldSupplyMarketRepository,
): YieldSupplyGetTokenStatusUseCase {
return YieldSupplyGetTokenStatusUseCase(
yieldSupplyMarketRepository = yieldSupplyMarketRepository,
)
}
}

View file

@ -8,11 +8,19 @@ import java.math.BigDecimal
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,
@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: Double,
@Json(name = "isActive") val isActive: Boolean,
@Json(name = "ltv") val ltv: Int,
@Json(name = "liquidationThreshold") val liquidationThreshold: Int,
@Json(name = "decimals") val decimals: Int,
@Json(name = "chainId") val chainId: Int,
@Json(name = "priority") val priority: Int,
@Json(name = "isEnabled") val isEnabled: Boolean,
@Json(name = "lastUpdatedAt") val lastUpdatedAt: String,
)

View file

@ -747,7 +747,7 @@
<string name="notification_referral_promo_text">Teile Deinen Code verdiene 5 USDT pro Verkauf. Deine Freunde erhalten 10 % Rabatt.</string>
<string name="notification_referral_promo_title">Erhalte BELOHNUNGEN für jeden Freund!</string>
<string name="notification_sepa_button">Krypto kaufen</string>
<string name="notification_sepa_text">Profitiere von null Gebühren beim Kauf von Kryptowährungen per SEPA-Überweisung.</string>
<string name="notification_sepa_text">Profitiere von **0 % Gebühren** beim Kauf von Kryptowährungen per SEPA-Überweisung.</string>
<string name="notification_sepa_title">Krypto mit SEPA kaufen</string>
<string name="onboarding_access_code_feature_1_description">Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen</string>
<string name="onboarding_access_code_feature_1_title">Schützen</string>

View file

@ -261,7 +261,6 @@
<string name="common_left">残り%1$s</string>
<string name="common_legacy_bitcoin_address">レガシービットコイン</string>
<string name="common_locked">ロックされています</string>
<string name="common_locked_wallets">ロックされたウォレット</string>
<string name="common_main_network">メインネットワーク</string>
<string name="common_month"></string>
<string name="common_network_fee_title">ネットワーク手数料</string>
@ -311,7 +310,6 @@
<string name="common_sign">署名</string>
<string name="common_sign_and_send">署名して送信</string>
<string name="common_skip">スキップ</string>
<string name="common_something_went_wrong">問題が発生しました</string>
<string name="common_stake">ステーキング</string>
<string name="common_staking">ステーキング</string>
<string name="common_start">始める</string>
@ -1148,7 +1146,7 @@
<string name="staking_notification_restake_rewards_text">獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。</string>
<string name="staking_notification_restake_text">再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。</string>
<string name="staking_notification_stake_entire_balance_text">残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。</string>
<string name="staking_notification_ton_activate_account">TONのステーキングを開始するには、まず自分のアドレスに少額の取引を送信します。これによりウォレットが有効になります。</string>
<string name="staking_notification_ton_activate_account">TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。</string>
<string name="staking_notification_ton_extra_reserve_info">取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。</string>
<string name="staking_notification_ton_extra_reserve_is_required">この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。</string>
<string name="staking_notification_ton_extra_reserve_title">TONの準備金が必要です</string>

View file

@ -1731,6 +1731,7 @@
<string name="yield_module_approve_sheet_title">Confirm approval</string>
<string name="yield_module_balance_info_sheet_subtitle">Text about your money in balance [PLACEHOLDER]</string>
<string name="yield_module_balance_info_sheet_title">Your %s is deposited in Aave</string>
<string name="yield_module_deposit_error_notification_title">The amount received, %1$s %2$s was not deposited to Aave.</string>
<string name="yield_module_earn_badge">Earn %1$s%%</string>
<string name="yield_module_earn_sheet_available_title">Available</string>
<string name="yield_module_earn_sheet_current_apy_title">Current APY</string>
@ -1746,6 +1747,7 @@
<string name="yield_module_fee_policy_sheet_max_fee_note">If network fees rise above maximum fee, the transaction wont go through until they decrease. You can change this limit later.</string>
<string name="yield_module_fee_policy_sheet_max_fee_title">Maximum fee</string>
<string name="yield_module_fee_policy_sheet_title">Fee policy</string>
<string name="yield_module_high_fee_error">Network fee is too high right now. Waiting until it falls below your limit.</string>
<string name="yield_module_main_view_approve_notification_description">Write description here. In one, two or three lines will be awesome. [PLACEHOLDER]</string>
<string name="yield_module_main_view_approve_notification_title">Some token approve needed</string>
<string name="yield_module_network_fee_unreachable_notification_description">Check your network connection</string>
@ -1784,4 +1786,6 @@
<string name="yield_module_transfer_mode_automatic">Automatic</string>
<string name="yield_module_unable_to_cover_fee_description">Deposit some %1$s %2$s to cover the network fee for transactions</string>
<string name="yield_module_unable_to_cover_fee_title">Unable to cover %s fee</string>
<string name="yield_module_unavailable_subtitle">The interest service isnt available at the moment. Please try again later.</string>
<string name="yield_module_unavailable_title">Earnings unavailable</string>
</resources>

View file

@ -4,8 +4,10 @@ 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.domain.yield.supply.YieldSupplyMarketRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldTokenStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
@ -28,4 +30,9 @@ internal class DefaultYieldSupplyMarketRepository(
}
override fun getMarketsFlow(): Flow<List<YieldMarketToken>> = store.get()
override suspend fun getTokenStatus(contractAddress: String): YieldTokenStatus {
val response = yieldSupplyApi.getYieldTokenStatus(contractAddress).getOrThrow()
return YieldTokenStatusConverter.convert(response)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.data.yield.supply.converters
import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse
import com.tangem.domain.yield.supply.models.YieldTokenStatus
import com.tangem.utils.converter.Converter
internal object YieldTokenStatusConverter : Converter<YieldTokenStatusResponse, YieldTokenStatus> {
override fun convert(value: YieldTokenStatusResponse): YieldTokenStatus {
return YieldTokenStatus(
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,
decimals = value.decimals,
chainId = value.chainId,
priority = value.priority,
isEnabled = value.isEnabled,
lastUpdatedAt = value.lastUpdatedAt,
)
}
}

View file

@ -4,17 +4,25 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
/**
* Domain model representing a user's yield status for a specific token.
* Domain model representing yield market 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,
val tokenName: String,
val apy: SerializedBigDecimal,
val totalSupplied: String,
val totalBorrowed: String,
val liquidityRate: String,
val borrowRate: String,
val utilizationRate: Double,
val isActive: Boolean,
val ltv: Int,
val liquidationThreshold: Int,
val decimals: Int,
val chainId: Int,
val priority: Int,
val isEnabled: Boolean,
val lastUpdatedAt: String,
)

View file

@ -1,6 +1,7 @@
package com.tangem.domain.yield.supply
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldTokenStatus
import kotlinx.coroutines.flow.Flow
interface YieldSupplyMarketRepository {
@ -20,4 +21,10 @@ interface YieldSupplyMarketRepository {
* Observe runtime markets updates.
*/
fun getMarketsFlow(): Flow<List<YieldMarketToken>>
/**
* Get yield token status by contract address.
*/
@Throws
suspend fun getTokenStatus(contractAddress: String): YieldTokenStatus
}

View file

@ -0,0 +1,15 @@
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.YieldTokenStatus
class YieldSupplyGetTokenStatusUseCase(
private val yieldSupplyMarketRepository: YieldSupplyMarketRepository,
) {
suspend operator fun invoke(token: CryptoCurrency.Token): Either<Throwable, YieldTokenStatus> = Either.catch {
yieldSupplyMarketRepository.getTokenStatus(token.contractAddress)
}
}

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.domain.tokens)
implementation(projects.domain.transaction.models)
implementation(projects.domain.transaction)
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.yieldSupply)
implementation(projects.domain.feedback.models)
implementation(projects.domain.feedback)

View file

@ -13,6 +13,8 @@ internal sealed class YieldSupplyUM {
data object Loading : YieldSupplyUM()
data object Unavailable : YieldSupplyUM()
data class Content(
val rewardsBalance: TextReference,
val rewardsApy: TextReference,

View file

@ -11,12 +11,14 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
@ -41,6 +43,7 @@ internal class YieldSupplyModel @Inject constructor(
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
) : Model(), YieldSupplyClickIntents {
private val params = paramsContainer.require<YieldSupplyComponent.Params>()
@ -99,6 +102,28 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun loadTokenStatus(cryptoCurrency: CryptoCurrency.Token) {
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrency)
.onRight { tokenStatus ->
val newState = if (tokenStatus.isActive) {
YieldSupplyUM.Initial(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList("5.1"),
),
onClick = ::onStartEarningClick,
)
} else {
YieldSupplyUM.Unavailable
}
uiState.update { newState }
}.onLeft {
uiState.update { YieldSupplyUM.Unavailable }
}
}
}
override fun onStartEarningClick() {
appRouter.push(
YieldSupplyPromo(
@ -148,16 +173,12 @@ internal class YieldSupplyModel @Inject constructor(
onClick = ::onActiveClick,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
)
else -> YieldSupplyUM.Initial(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList("5.1"),
),
onClick = ::onStartEarningClick,
)
else -> YieldSupplyUM.Loading
}
uiState.update { yieldSupplyUM }
(cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus)
}
private companion object {

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
@ -52,55 +53,36 @@ internal fun YieldSupplyBlockContent(
YieldSupplyUM.Processing.Exit -> SupplyProcessing(
resourceReference(R.string.yield_module_stop_earning),
)
YieldSupplyUM.Unavailable -> SupplyUnavailable()
}
}
}
@Composable
private fun SupplyInitial(supplyUM: YieldSupplyUM.Initial) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.primary)
.padding(12.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_analytics_up_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
modifier = Modifier
.background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape)
.padding(6.dp)
.size(24.dp),
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = supplyUM.title.resolveReference(),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
Text(
text = stringResourceSafe(R.string.yield_module_token_details_earn_notification_description),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
SupplyInfo(
title = supplyUM.title,
subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description),
iconTint = TangemTheme.colors.icon.accent,
button = {
SecondaryButton(
text = stringResourceSafe(R.string.common_learn_more),
onClick = supplyUM.onClick,
size = TangemButtonSize.WideAction,
modifier = Modifier.fillMaxWidth(),
)
},
)
}
@Composable
private fun SupplyUnavailable() {
SupplyInfo(
title = resourceReference(R.string.yield_module_unavailable_title),
subtitle = resourceReference(R.string.yield_module_unavailable_subtitle),
iconTint = TangemTheme.colors.icon.inactive,
button = null,
)
}
@Composable
@ -199,26 +181,101 @@ private fun SupplyProcessing(text: TextReference) {
@Composable
private fun SupplyLoading() {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.primary)
.padding(12.dp),
) {
Text(
text = stringResourceSafe(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_analytics_up_24),
contentDescription = null,
tint = TangemTheme.colors.icon.inactive,
modifier = Modifier
.background(TangemTheme.colors.icon.inactive.copy(alpha = 0.1f), CircleShape)
.padding(6.dp)
.size(24.dp),
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
TextShimmer(
text = stringResourceSafe(R.string.yield_module_unavailable_title),
style = TangemTheme.typography.button,
)
TextShimmer(
style = TangemTheme.typography.subtitle2,
text = stringResourceSafe(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.yield_module_unavailable_subtitle),
style = TangemTheme.typography.caption2,
)
TextShimmer(
modifier = Modifier.width(100.dp),
text = stringResourceSafe(R.string.yield_module_unavailable_subtitle),
style = TangemTheme.typography.caption2,
)
}
}
SecondaryButton(
text = stringResourceSafe(R.string.common_learn_more),
onClick = { },
showProgress = true,
enabled = false,
size = TangemButtonSize.WideAction,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun SupplyInfo(
title: TextReference,
subtitle: TextReference,
iconTint: Color,
modifier: Modifier = Modifier,
button: (@Composable () -> Unit)? = null,
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.primary)
.padding(12.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_analytics_up_24),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.background(iconTint.copy(alpha = 0.1f), CircleShape)
.padding(6.dp)
.size(24.dp),
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
button?.invoke()
}
}
@ -257,6 +314,7 @@ private class PreviewProvider : PreviewParameterProvider<YieldSupplyUM> {
YieldSupplyUM.Loading,
YieldSupplyUM.Processing.Enter,
YieldSupplyUM.Processing.Exit,
YieldSupplyUM.Unavailable,
)
}
// endregion