Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 20:52:31 +05:00
parent a821732662
commit 30431a3dbb
20 changed files with 540 additions and 34 deletions

View file

@ -3,6 +3,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
@ -132,4 +134,18 @@ internal object YieldSupplyDomainModule {
yieldSupplyRepository = yieldSupplyRepository,
)
}
@Provides
@Singleton
fun provideYieldSupplyMinAmountUseCase(
feeRepository: FeeRepository,
quotesRepository: QuotesRepository,
currenciesRepository: CurrenciesRepository,
): YieldSupplyMinAmountUseCase {
return YieldSupplyMinAmountUseCase(
feeRepository = feeRepository,
quotesRepository = quotesRepository,
currenciesRepository = currenciesRepository,
)
}
}

View file

@ -21,6 +21,8 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.domain.blockaid.models)
implementation(projects.domain.blockaid)
implementation(projects.domain.quotes)
implementation(projects.domain.tokens)
/** Tandem SDK */
implementation(tangemDeps.blockchain)

View file

@ -0,0 +1,88 @@
package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
class YieldSupplyMinAmountUseCase(
private val feeRepository: FeeRepository,
private val quotesRepository: QuotesRepository,
private val currenciesRepository: CurrenciesRepository,
) {
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<Throwable, BigDecimal> = catch {
val feeWithoutGas = feeRepository.getEthereumFeeWithoutGas(userWallet, cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate ?: error("Fiat rate is missing")
require(fiatRate > BigDecimal.ZERO) { "Fiat rate for token must be > 0" }
val nativeCryptoCurrency = currenciesRepository.getNetworkCoin(
userWalletId = userWallet.walletId,
networkId = cryptoCurrencyStatus.currency.network.id,
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
)
val quotes =
quotesRepository.getMultiQuoteSyncOrNull(setOfNotNull(nativeCryptoCurrency.id.rawCurrencyId))
?: error("Quotes for native coin are unavailable")
val quotesStatus = quotes.firstOrNull() ?: error("Empty quotes list for native coin")
val nativeFiatRate = (quotesStatus.value as? QuoteStatus.Data)?.fiatRate
?: error("Native fiat rate is missing")
require(nativeFiatRate > BigDecimal.ZERO) { "Native fiat rate must be > 0" }
val nativeGas = feeWithoutGas.fixFee(nativeCryptoCurrency, ETHEREUM_CONSTANT_GAS_LIMIT)
val rateRatio = nativeFiatRate.divide(
fiatRate,
cryptoCurrencyStatus.currency.decimals,
RoundingMode.HALF_UP,
)
val tokenValue = rateRatio.multiply(nativeGas.amount.value)
val feeBuffered = tokenValue.multiply(FEE_BUFFER_MULTIPLIER)
feeBuffered
.divide(MAX_FEE_PERCENT, cryptoCurrencyStatus.currency.decimals, RoundingMode.HALF_UP)
.stripTrailingZeros()
}
private fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger) = when (this) {
is Fee.Ethereum.Legacy -> copy(
gasLimit = gasLimit,
amount = amount.copy(
value = gasPrice.multiply(gasLimit)
.toBigDecimal().movePointLeft(cryptoCurrency.decimals),
),
)
is Fee.Ethereum.EIP1559 -> copy(
gasLimit = gasLimit,
amount = amount.copy(
value = maxFeePerGas.multiply(gasLimit)
.toBigDecimal().movePointLeft(cryptoCurrency.decimals),
),
)
else -> this
}
private companion object {
val FEE_BUFFER_MULTIPLIER: BigDecimal = BigDecimal("1.25")
val MAX_FEE_PERCENT: BigDecimal = BigDecimal("0.04")
val ETHEREUM_CONSTANT_GAS_LIMIT = 350_000.toBigInteger()
}
}

View file

@ -0,0 +1,247 @@
package com.tangem.domain.yield.supply
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
class YieldSupplyMinAmountUseCaseTest {
private val feeRepository: FeeRepository = mockk(relaxed = true)
private val quotesRepository: QuotesRepository = mockk(relaxed = true)
private val currenciesRepository: CurrenciesRepository = mockk(relaxed = true)
private val useCase = YieldSupplyMinAmountUseCase(
feeRepository = feeRepository,
quotesRepository = quotesRepository,
currenciesRepository = currenciesRepository,
)
@Test
fun `GIVEN valid inputs WHEN invoke THEN return expected min amount`() = runTest {
val network = createNetwork()
val nativeCoin = createNativeCoin(network)
val token = createToken(network)
val tokenFiatRate = BigDecimal("1.00")
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loaded(
amount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
fiatRate = tokenFiatRate,
priceChange = BigDecimal.ZERO,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
val userWallet = createUserWallet()
val maxFeePerGas = BigInteger("158320679232")
val fee = createEip1559Fee(maxFeePerGas)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWallet, token) } returns fee
coEvery {
currenciesRepository.getNetworkCoin(
userWalletId = userWallet.walletId,
networkId = token.network.id,
derivationPath = token.network.derivationPath,
)
} returns nativeCoin
val nativeFiatRate = BigDecimal("0.20353756561552608")
coEvery {
quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!))
} returns setOf(
QuoteStatus(
rawCurrencyId = CryptoCurrency.RawID("polygon-ecosystem-token"),
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
priceChange = BigDecimal("0.09000000000000007"),
),
),
)
val expected = expectedMinAmount(maxFeePerGas, nativeFiatRate, tokenFiatRate, token.decimals)
val result = useCase(userWallet, tokenStatus).getOrNull()
Truth.assertThat(result).isEqualTo(expected)
}
@Test
fun `GIVEN missing token fiat rate WHEN invoke THEN return left with error`() = runTest {
val network = createNetwork()
val token = createToken(network)
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.ZERO,
fiatAmount = null,
fiatRate = null,
priceChange = null,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
val userWallet = createUserWallet()
val result = useCase(userWallet, tokenStatus)
Truth.assertThat(result.isLeft()).isTrue()
Truth.assertThat(result.leftOrNull()?.message).isEqualTo("Fiat rate is missing")
}
@Test
fun `GIVEN quotes unavailable WHEN invoke THEN return left with error`() = runTest {
val network = createNetwork()
val nativeCoin = createNativeCoin(network)
val token = createToken(network)
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
val userWallet = createUserWallet()
val maxFeePerGas = BigInteger("158320679232")
val fee = createEip1559Fee(maxFeePerGas)
coEvery { feeRepository.getEthereumFeeWithoutGas(userWallet, token) } returns fee
coEvery {
currenciesRepository.getNetworkCoin(
userWalletId = userWallet.walletId,
networkId = token.network.id,
derivationPath = token.network.derivationPath,
)
} returns nativeCoin
coEvery {
quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!))
} returns null
val result = useCase(userWallet, tokenStatus)
Truth.assertThat(result.isLeft()).isTrue()
Truth.assertThat(result.leftOrNull()?.message).isEqualTo("Quotes for native coin are unavailable")
}
private fun createNetwork(): Network {
val derivationPath = Network.DerivationPath.None
return Network(
id = Network.ID(Network.RawID("polygon"), derivationPath),
backendId = "polygon",
name = "Polygon",
currencySymbol = "MATIC",
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.ENS,
)
}
private fun createNativeCoin(network: Network): CryptoCurrency.Coin {
val nativeCoinId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(network.rawId),
suffix = CryptoCurrency.ID.Suffix.RawID("polygon-ecosystem-token"),
)
return CryptoCurrency.Coin(
id = nativeCoinId,
network = network,
name = "Polygon",
symbol = "MATIC",
decimals = 18,
iconUrl = null,
isCustom = false,
)
}
private fun createToken(network: Network): CryptoCurrency.Token {
val tokenId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(network.rawId),
suffix = CryptoCurrency.ID.Suffix.RawID("test-token", "0xContract"),
)
return CryptoCurrency.Token(
id = tokenId,
network = network,
name = "Test Token",
symbol = "TT",
decimals = 18,
iconUrl = null,
isCustom = false,
contractAddress = "0xContract",
)
}
private fun createUserWallet(): UserWallet {
val wallet = mockk<UserWallet>(relaxed = true)
every { wallet.walletId } returns UserWalletId("001122")
return wallet
}
private fun createEip1559Fee(maxFeePerGas: BigInteger): Fee.Ethereum.EIP1559 {
return Fee.Ethereum.EIP1559(
amount = Amount(value = BigDecimal.ZERO, blockchain = Blockchain.Ethereum),
gasLimit = BigInteger.ZERO,
maxFeePerGas = maxFeePerGas,
priorityFee = BigInteger.ZERO,
)
}
private fun expectedMinAmount(
maxFeePerGas: BigInteger,
nativeFiatRate: BigDecimal,
tokenFiatRate: BigDecimal,
decimals: Int,
): BigDecimal {
val gasLimit = BigInteger("350000")
val nativeGas = maxFeePerGas.multiply(gasLimit).toBigDecimal().movePointLeft(decimals)
val rateRatio = nativeFiatRate.divide(tokenFiatRate, decimals, RoundingMode.HALF_UP)
val tokenValue = rateRatio.multiply(nativeGas)
val feeBuffered = tokenValue.multiply(BigDecimal("1.25"))
return feeBuffered
.divide(BigDecimal("0.04"), decimals, RoundingMode.HALF_UP)
.stripTrailingZeros()
}
}

View file

@ -16,6 +16,7 @@ internal sealed class YieldSupplyFeeUM {
val feeValue: TextReference,
val currentNetworkFeeValue: TextReference,
val maxNetworkFeeValue: TextReference,
val minAmountFeeValue: TextReference,
) : YieldSupplyFeeUM()
}

View file

@ -0,0 +1,30 @@
package com.tangem.features.yield.supply.impl.common.formatter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.utils.StringsSigns
import java.math.BigDecimal
internal class YieldSupplyMinAmountFormatter(
private val feeCryptoCurrency: CryptoCurrency,
private val appCurrency: AppCurrency,
) {
operator fun invoke(feeValue: BigDecimal, fiatRate: BigDecimal?): TextReference {
val cryptoFee = feeValue.format { crypto(feeCryptoCurrency) }
val fiatFeeValue = fiatRate?.let(feeValue::multiply)
val fiatFee = fiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
return combinedReference(
stringReference(cryptoFee),
stringReference(" ${StringsSigns.DOT} "),
stringReference(fiatFee),
)
}
}

View file

@ -141,6 +141,7 @@ private class YieldSupplyActionContentPreviewProvider : PreviewParameterProvider
feeValue = stringReference("0.00020 ETH • \$0.99"),
currentNetworkFeeValue = stringReference("1.45 USDT • \$1.45"),
maxNetworkFeeValue = stringReference("8.50 USDT • \$8.50"),
minAmountFeeValue = stringReference("50 USDT • \$50"),
),
isPrimaryButtonEnabled = false,
isTransactionSending = false,

View file

@ -203,7 +203,7 @@ internal class YieldSupplyModel @Inject constructor(
resourceReference(
R.string.yield_module_token_details_earn_notification_apy,
),
stringReference(tokenStatus.apy.toString() + "%"),
stringReference(" ${tokenStatus.apy}%"),
),
onClick = ::onActiveClick,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,

View file

@ -42,10 +42,14 @@ internal class YieldSupplyPromoModel @Inject constructor(
}
override fun onHowItWorksClick() {
urlOpener.openUrl("https://tangem.com/") // TODO replace with real link
urlOpener.openUrl(HOW_IT_WORKS_URL)
}
override fun onStartEarningClick() {
bottomSheetNavigation.activate(YieldSupplyPromoConfig.Action)
}
companion object {
private const val HOW_IT_WORKS_URL = "https://tangem.com/en/blog/post/savings-account "
}
}

View file

@ -11,4 +11,5 @@ internal data class YieldSupplyActiveContentUM(
val subtitleLink: TextReference,
val notificationUM: NotificationUM?,
val apy: TextReference? = null,
val minAmount: TextReference?,
)

View file

@ -1,5 +1,6 @@
package com.tangem.features.yield.supply.impl.subcomponents.active.model
import arrow.core.getOrElse
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -11,12 +12,17 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetProtocolBalanceUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyMinAmountFormatter
import com.tangem.features.yield.supply.impl.subcomponents.active.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -29,12 +35,15 @@ internal class YieldSupplyActiveModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val yieldSupplyGetProtocolBalanceUseCase: YieldSupplyGetProtocolBalanceUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : Model() {
private val params: YieldSupplyActiveComponent.Params = paramsContainer.require()
private val cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow
private val cryptoCurrency = cryptoCurrencyStatusFlow.value.currency
private var appCurrency = AppCurrency.Default
val uiState: StateFlow<YieldSupplyActiveContentUM>
field = MutableStateFlow(
@ -48,6 +57,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
),
subtitleLink = resourceReference(R.string.common_read_more),
notificationUM = null,
minAmount = null,
),
)
@ -55,6 +65,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
subscribeOnCurrencyUpdates()
modelScope.launch(dispatchers.default) {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
val protocolBalance = yieldSupplyGetProtocolBalanceUseCase(
userWalletId = params.userWallet.walletId,
cryptoCurrency = cryptoCurrency,
@ -93,6 +104,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
}
loadApy()
loadMinAmount()
uiState.update {
it.copy(
@ -126,6 +138,27 @@ internal class YieldSupplyActiveModel @Inject constructor(
}
}
private fun loadMinAmount() {
modelScope.launch(dispatchers.default) {
yieldSupplyMinAmountUseCase(
params.userWallet,
cryptoCurrencyStatusFlow.value,
).onRight { minAmount ->
val minAmountTextReference = YieldSupplyMinAmountFormatter(
cryptoCurrencyStatusFlow.value.currency,
appCurrency,
).invoke(minAmount, cryptoCurrencyStatusFlow.value.value.fiatRate)
uiState.update {
it.copy(minAmount = minAmountTextReference)
}
}.onLeft {
uiState.update {
it.copy(minAmount = TextReference.Str(DASH_SIGN))
}
}
}
}
private companion object {
const val AAVEV3_PREFIX = "a"
}

View file

@ -59,7 +59,6 @@ internal fun YieldSupplyActiveContent(
CurrentApy(state.apy)
chartComponent.Content(Modifier.padding(bottom = 12.dp))
}
YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden)
AnimatedVisibility(state.notificationUM != null) {
val wrappedNotification = remember(this) { requireNotNull(state.notificationUM) }
@ -69,6 +68,8 @@ internal fun YieldSupplyActiveContent(
containerColor = TangemTheme.colors.background.action,
)
}
YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden)
}
}
@ -163,6 +164,15 @@ private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanc
info = state.availableBalance,
isBalanceHidden = isBalanceHidden,
)
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
)
InfoRow(
title = resourceReference(R.string.yield_module_fee_policy_sheet_min_amount_title),
info = state.minAmount,
isBalanceHidden = false,
)
}
}
@ -251,6 +261,7 @@ private class YieldSupplyActiveBottomSheetPreviewProvider : PreviewParameterProv
subtitleLink = resourceReference(R.string.common_read_more),
notificationUM = NotificationUM.Error.InvalidAmount,
apy = stringReference("5,14%"),
minAmount = stringReference("50 USDT"),
),
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.yield.supply.impl.subcomponents.approve.model
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -194,40 +196,47 @@ internal class YieldSupplyApproveModel @Inject constructor(
}
},
ifRight = { fee ->
val feeCryptoValue = fee.normal.amount.value
applyFee(fee, approvalTransitionData)
},
)
}
val feeFiatValue = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
feeCryptoValue?.multiply(rate)
}
val cryptoFee = feeCryptoValue.format { crypto(feeCryptoCurrencyStatus.currency) }
val fiatFee = feeFiatValue.format { fiat(appCurrency.code, appCurrency.symbol) }
private suspend fun applyFee(transactionFee: TransactionFee, approvalTransitionData: TransactionData.Uncompiled) {
val feeCryptoValue = transactionFee.normal.amount.value
uiState.update {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
it.copy(
isPrimaryButtonEnabled = true,
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = persistentListOf(approvalTransitionData.copy(fee = fee.normal)),
feeValue = combinedReference(
stringReference(cryptoFee),
stringReference(" $DOT "),
stringReference(fiatFee),
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
),
)
}
}
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = feeCryptoValue,
feeError = null,
val feeFiatValue = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
feeCryptoValue?.multiply(rate)
}
val cryptoFee = feeCryptoValue.format { crypto(feeCryptoCurrencyStatus.currency) }
val fiatFee = feeFiatValue.format { fiat(appCurrency.code, appCurrency.symbol) }
uiState.update {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
it.copy(
isPrimaryButtonEnabled = true,
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = persistentListOf(
approvalTransitionData.copy(fee = transactionFee.normal),
),
feeValue = combinedReference(
stringReference(cryptoFee),
stringReference(" $DOT "),
stringReference(fiatFee),
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
minAmountFeeValue = TextReference.EMPTY,
),
)
},
}
}
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = feeCryptoValue,
feeError = null,
),
)
}

View file

@ -29,6 +29,7 @@ import com.tangem.features.yield.supply.impl.common.ui.YieldSupplyFeeRow
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongMethod")
@Composable
internal fun YieldSupplyFeePolicyContent(
yieldSupplyFeeUM: YieldSupplyFeeUM,
@ -61,6 +62,28 @@ internal fun YieldSupplyFeePolicyContent(
modifier = Modifier.padding(horizontal = 16.dp),
)
SpacerH24()
FooterContainer(
footer = resourceReference(
id = R.string.yield_module_fee_policy_sheet_min_amount_note,
formatArgs = wrappedList(networkName),
),
paddingValues = PaddingValues(
top = 8.dp,
start = 12.dp,
end = 12.dp,
),
) {
val minAmount = when (yieldSupplyFeeUM) {
is YieldSupplyFeeUM.Content -> yieldSupplyFeeUM.minAmountFeeValue
YieldSupplyFeeUM.Error -> stringReference(StringsSigns.DASH_SIGN)
YieldSupplyFeeUM.Loading -> null
}
YieldSupplyFeeRow(
title = resourceReference(R.string.yield_module_fee_policy_sheet_min_amount_title),
value = minAmount,
)
}
SpacerH16()
FooterContainer(
footer = resourceReference(
id = R.string.yield_module_fee_policy_sheet_current_fee_note,
@ -101,6 +124,18 @@ internal fun YieldSupplyFeePolicyContent(
value = maxFee,
)
}
Text(
text = stringResourceSafe(R.string.yield_module_fee_policy_tangem_service_fee_title),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(
top = 8.dp,
start = 12.dp,
end = 12.dp,
),
)
}
}
@ -116,11 +151,11 @@ private fun YieldSupplyFeePolicyContent_Preview() {
feeValue = stringReference("0.0001 ETH • \$1.45"),
maxNetworkFeeValue = stringReference("8.50 USDT • \$8.50"),
currentNetworkFeeValue = stringReference("1.45 USDT • \$1.45"),
minAmountFeeValue = stringReference("50 USDT • \$50"),
),
tokenSymbol = "USDT",
networkName = "Ethereum",
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}

View file

@ -117,6 +117,7 @@ internal class YieldSupplyStartEarningComponent(
onClick = model::onClick,
enabled = state.isPrimaryButtonEnabled,
iconResId = icon,
showProgress = state.isTransactionSending,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),

View file

@ -22,6 +22,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
@ -61,11 +62,13 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require()
private val cryptoCurrency = params.cryptoCurrency
private var minAmount: BigDecimal by Delegates.notNull()
var userWallet: UserWallet by Delegates.notNull()
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
@ -112,6 +115,14 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
}
}
private suspend fun calculateMinAmount(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus) {
yieldSupplyMinAmountUseCase(userWallet, cryptoCurrencyStatus).onRight {
minAmount = it
}.onLeft {
minAmount = BigDecimal.ZERO
}
}
private suspend fun getMaxFee(): BigDecimal? {
if (uiState.value.maxFee != BigDecimal.ZERO) return uiState.value.maxFee
val yieldTokenStatus = yieldSupplyGetTokenStatusUseCase(cryptoCurrency as CryptoCurrency.Token)
@ -166,6 +177,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
updatedTransactionList = updatedTransactionList,
feeValue = feeSum,
maxNetworkFee = maxFee,
minAmount = minAmount,
),
)
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
@ -276,6 +288,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
feeCryptoCurrencyStatusFlow.update { feeCurrencyStatus }
modelScope.launch {
calculateMinAmount(userWallet, currencyStatus)
onLoadFee()
}
}

View file

@ -10,12 +10,14 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyMinAmountFormatter
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
import java.math.RoundingMode
@Suppress("LongParameterList")
internal class YieldSupplyStartEarningFeeContentTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
@ -23,6 +25,7 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
private val updatedTransactionList: List<TransactionData.Uncompiled>,
private val feeValue: BigDecimal,
private val maxNetworkFee: BigDecimal,
private val minAmount: BigDecimal,
) : Transformer<YieldSupplyActionUM> {
override fun transform(prevState: YieldSupplyActionUM): YieldSupplyActionUM {
val cryptoCurrency = cryptoCurrencyStatus.currency
@ -46,6 +49,11 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
}
val maxFiatFee = maxFiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
val minAmountTextReference = YieldSupplyMinAmountFormatter(
cryptoCurrency,
appCurrency,
).invoke(minAmount, cryptoCurrencyStatus.value.fiatRate)
return if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
prevState.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
@ -67,6 +75,7 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
stringReference(" $DOT "),
stringReference(maxFiatFee),
),
minAmountFeeValue = minAmountTextReference,
),
)
}

View file

@ -92,6 +92,7 @@ internal class YieldSupplyStopEarningComponent(
onClick = model::onClick,
iconResId = walletInterationIcon(params.userWallet),
enabled = state.isPrimaryButtonEnabled,
showProgress = state.isTransactionSending,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),

View file

@ -12,6 +12,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
@ -51,6 +52,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger,
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require()
@ -136,6 +138,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
)
},
ifRight = {
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id)
yieldSupplyDeactivateUseCase(cryptoCurrency)
params.callback.onTransactionSent()
},

View file

@ -44,6 +44,7 @@ internal class YieldSupplyStopEarningFeeContentTransformer(
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
minAmountFeeValue = TextReference.EMPTY,
),
)
}