Updated on 2026-08-14
This commit is contained in:
parent
a821732662
commit
30431a3dbb
20 changed files with 540 additions and 34 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ internal sealed class YieldSupplyFeeUM {
|
|||
val feeValue: TextReference,
|
||||
val currentNetworkFeeValue: TextReference,
|
||||
val maxNetworkFeeValue: TextReference,
|
||||
val minAmountFeeValue: TextReference,
|
||||
) : YieldSupplyFeeUM()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
}
|
||||
}
|
||||
|
|
@ -11,4 +11,5 @@ internal data class YieldSupplyActiveContentUM(
|
|||
val subtitleLink: TextReference,
|
||||
val notificationUM: NotificationUM?,
|
||||
val apy: TextReference? = null,
|
||||
val minAmount: TextReference?,
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ internal class YieldSupplyStartEarningComponent(
|
|||
onClick = model::onClick,
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
iconResId = icon,
|
||||
showProgress = state.isTransactionSending,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ internal class YieldSupplyStopEarningFeeContentTransformer(
|
|||
),
|
||||
currentNetworkFeeValue = TextReference.EMPTY,
|
||||
maxNetworkFeeValue = TextReference.EMPTY,
|
||||
minAmountFeeValue = TextReference.EMPTY,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue