diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index 839504ffbe..de38050dcf 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/domain/yield-supply/build.gradle.kts b/domain/yield-supply/build.gradle.kts index bc5d9b6904..b2648f08f0 100644 --- a/domain/yield-supply/build.gradle.kts +++ b/domain/yield-supply/build.gradle.kts @@ -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) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt new file mode 100644 index 0000000000..b544b7fe62 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt @@ -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 = 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() + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt new file mode 100644 index 0000000000..c140d8f35e --- /dev/null +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt @@ -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(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() + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt index d00eede2f9..ffac0fef60 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt @@ -16,6 +16,7 @@ internal sealed class YieldSupplyFeeUM { val feeValue: TextReference, val currentNetworkFeeValue: TextReference, val maxNetworkFeeValue: TextReference, + val minAmountFeeValue: TextReference, ) : YieldSupplyFeeUM() } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyMinAmountFormatter.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyMinAmountFormatter.kt new file mode 100644 index 0000000000..5d7c6dae16 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyMinAmountFormatter.kt @@ -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), + ) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt index aea87550b9..03437a23fe 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyActionContent.kt @@ -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, 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 5b5f8676da..243ae32425 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 @@ -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, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt index 9dbc5ecdd9..5d9144de30 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt @@ -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 " + } } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt index bf0fdc6e5b..7b1e55f50a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt @@ -11,4 +11,5 @@ internal data class YieldSupplyActiveContentUM( val subtitleLink: TextReference, val notificationUM: NotificationUM?, val apy: TextReference? = null, + val minAmount: TextReference?, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt index ae85f3f637..1241d2cebe 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt @@ -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 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" } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt index eab1197a0e..a20183fb10 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt @@ -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"), ), ) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index 0e7f24f5a7..4c3f65d7f9 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -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, + ), ) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/feepolicy/ui/YieldSupplyFeePolicyContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/feepolicy/ui/YieldSupplyFeePolicyContent.kt index b90e1b09b4..b9fc7b1d23 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/feepolicy/ui/YieldSupplyFeePolicyContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/feepolicy/ui/YieldSupplyFeePolicyContent.kt @@ -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), - ) } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt index 3d02037617..1783ee53c8 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt @@ -117,6 +117,7 @@ internal class YieldSupplyStartEarningComponent( onClick = model::onClick, enabled = state.isPrimaryButtonEnabled, iconResId = icon, + showProgress = state.isTransactionSending, modifier = Modifier .fillMaxWidth() .padding(16.dp), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 4a5568cfe5..d688a82ba8 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -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 @@ -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() } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt index 0e792d8529..5e0ba9a4a9 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/transformers/YieldSupplyStartEarningFeeContentTransformer.kt @@ -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, private val feeValue: BigDecimal, private val maxNetworkFee: BigDecimal, + private val minAmount: BigDecimal, ) : Transformer { 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, ), ) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt index 0db25f5017..2379649d45 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt @@ -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), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index ff7bb1fa17..05545b93de 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -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() }, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/transformer/YieldSupplyStopEarningFeeContentTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/transformer/YieldSupplyStopEarningFeeContentTransformer.kt index 0e3dbcac3d..4e44bb47e6 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/transformer/YieldSupplyStopEarningFeeContentTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/transformer/YieldSupplyStopEarningFeeContentTransformer.kt @@ -44,6 +44,7 @@ internal class YieldSupplyStopEarningFeeContentTransformer( ), currentNetworkFeeValue = TextReference.EMPTY, maxNetworkFeeValue = TextReference.EMPTY, + minAmountFeeValue = TextReference.EMPTY, ), ) }