From fbfd7476fafc68b0b3ac1312b4be4d026850555f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Oct 2025 13:01:49 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/YieldSupplyDomainModule.kt | 28 ++++++ .../api/common/config/YieldSupply.kt | 24 +++-- .../config/environment/EnvironmentConfig.kt | 1 + .../converter/EnvironmentConfigConverter.kt | 1 + .../models/EnvironmentConfigModel.kt | 1 + .../managers/MockEnvironmentConfigStorage.kt | 2 + core/res/src/main/res/values-ja/strings.xml | 33 ++++++- core/res/src/main/res/values/strings.xml | 8 +- .../domain/yield/supply/FeeExtensions.kt | 23 +++++ .../domain/yield/supply/YieldSupplyConst.kt | 5 ++ .../YieldSupplyEstimateEnterFeeUseCase.kt | 21 +---- .../YieldSupplyGetCurrentFeeUseCase.kt | 65 ++++++++++++++ .../usecase/YieldSupplyGetMaxFeeUseCase.kt | 67 ++++++++++++++ .../usecase/YieldSupplyMinAmountUseCase.kt | 29 ++---- ...atter.kt => YieldSupplyAmountFormatter.kt} | 24 +++-- .../impl/common/ui/YieldSupplyFeeRow.kt | 2 + .../entity/YieldSupplyActiveContentUM.kt | 5 +- .../active/model/YieldSupplyActiveModel.kt | 64 ++++++++++++- .../active/ui/YieldSupplyActiveContent.kt | 89 ++++++++++++++++++- ...SupplyStartEarningFeeContentTransformer.kt | 4 +- 20 files changed, 430 insertions(+), 66 deletions(-) create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyConst.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetMaxFeeUseCase.kt rename features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/{YieldSupplyMinAmountFormatter.kt => YieldSupplyAmountFormatter.kt} (53%) 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 de38050dcf..ea0ad78e44 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 @@ -148,4 +148,32 @@ internal object YieldSupplyDomainModule { currenciesRepository = currenciesRepository, ) } + + @Provides + @Singleton + fun provideYieldSupplyGetCurrentFeeUseCase( + feeRepository: FeeRepository, + quotesRepository: QuotesRepository, + currenciesRepository: CurrenciesRepository, + ): YieldSupplyGetCurrentFeeUseCase { + return YieldSupplyGetCurrentFeeUseCase( + feeRepository = feeRepository, + quotesRepository = quotesRepository, + currenciesRepository = currenciesRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyGetMaxFeeUseCase( + yieldSupplyRepository: YieldSupplyRepository, + quotesRepository: QuotesRepository, + currenciesRepository: CurrenciesRepository, + ): YieldSupplyGetMaxFeeUseCase { + return YieldSupplyGetMaxFeeUseCase( + yieldSupplyRepository = yieldSupplyRepository, + quotesRepository = quotesRepository, + currenciesRepository = currenciesRepository, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index 0665ec199b..6b7d1ab493 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -41,32 +41,44 @@ internal class YieldSupply( private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.DEV), ) private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.STAGE, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.STAGE), ) private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.MOCK, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.MOCK), ) private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://yield.tangem.org/", - headers = createHeaders(), + headers = createHeaders(ApiEnvironment.PROD), ) - private fun createHeaders() = buildMap { + private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap { put(key = "api-key", value = ProviderSuspend { - environmentConfigStorage.getConfigSync().yieldModuleApiKey.orEmpty() + getApiKey(apiEnvironment) }) putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values) putAll(from = RequestHeader.AuthenticationHeader(authProvider).values) } + + private fun getApiKey(apiEnvironment: ApiEnvironment): String { + return when (apiEnvironment) { + ApiEnvironment.MOCK, + ApiEnvironment.DEV, + ApiEnvironment.DEV_2, + ApiEnvironment.DEV_3, + ApiEnvironment.STAGE, + -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev + ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey + } ?: error("No tangem tech api config provided") + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index 384af56181..252782f20a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -21,4 +21,5 @@ data class EnvironmentConfig( val tangemApiKeyDev: String? = null, val tangemApiKeyStage: String? = null, val yieldModuleApiKey: String? = null, + val yieldModuleApiKeyDev: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt index cf52beb064..5c43a7c952 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt @@ -30,6 +30,7 @@ internal object EnvironmentConfigConverter : Converterすでにアクティブアカウントの上限(20件)に達しています。復元するには、1つをアーカイブしてください。 アカウントを復元できません アーカイブ済み + アカウントをアーカイブできませんでした。しばらくしてからもう一度お試しください。 アカウントを作成できませんでした。しばらくしてからもう一度お試しください。 アカウントを作成しました アカウントをアーカイブする @@ -249,10 +250,12 @@ 遅い 速度と料金 終了 + 忘れる 無料 送信元 アドレスを同期する はじめる + トークンを取得 プロバイダーへ移動 トークンへ移動 わかりました @@ -526,14 +529,20 @@ アクセスコードでアプリを保護して、設定を完了してください。 そうした場合は、最初からやり直す必要があります。 本当にアクティベーション処理を終了してもよろしいですか? + 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップから既存のウォレットを復元する Googleドライブのバックアップ + さらに強固なセキュリティのために、新しいウォレットを作成して資産を移動しましょう。 + 新しいウォレットを作成 Tangemの高性能ハードウェアウォレットで、セキュリティをさらに強化しましょう。 ハードウェアウォレット + 現在のウォレットをTangemウォレットに移します。 + 現在のウォレットをアップグレードする バックアップへ移動 アクセスコードを作成する前にウォレットをバックアップしてください。 まずバックアップを完了する + その他の方法 秘密鍵をオフラインで安全に保存する物理デバイス。 リカバリーフレーズ 鍵はアプリに保存されます @@ -603,6 +612,7 @@ 選択したトークンは現在、暗号資産ウォレット内でのアクションには利用できません。しかし、心配しないでください。賛成票を投じることで関心を表明できます。 賛成票を投じる ウォレットは複数のネットワークをサポートしていません。 + コインについて このアセットを購入・交換・受け取るには、ポートフォリオに追加してください。 このアセットは現在ウォレットで利用できません このアセットはこのウォレットでは使用できません。 @@ -638,6 +648,7 @@ トレンド ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s 最大%s APYを獲得 + トークンを追加しました %sについて %d取引所 @@ -895,7 +906,7 @@ 最大%d日 %s分 - 下記より利用可能 + 利用可能: 以下が手に入ります。 サービスは外部プロバイダーによって提供されます。 \nTangemは責任を負いません。 この画面を閉じて、トークンの詳細画面で取引状況を確認できます。 @@ -1275,7 +1286,20 @@ 受け取る トークンを選択 利用不可 + 入金 + 異議申し立て + 取引を表示 + サービス手数料 + 手数料 + 完了 + 拒否 + 保留中 + 銀行がこの取引リクエストを拒否しました。 + この手数料は、送金処理にかかるコストをカバーするためのものです。 + 出金 + PINを変更する データの読み込みに失敗しました。しばらくしてからもう一度お試しください。 + カードの一時停止 非表示 技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。 現在、受け取りは利用できません @@ -1310,6 +1334,7 @@ ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。 %sの売却は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 %sのステーキングは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 + ここにテキストを入力 XPUBを生成する 非表示 このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。 @@ -1729,10 +1754,10 @@ 今後の%sの入金はすべて、取引手数料が差し引かれて自動的にAaveに供給されます。 ネットワーク手数料が上限手数料を超えた場合、手数料が下がるまで取引は成立しません。この制限は後で変更できます。 最大手数料 - 取引手数料は、預入額の4%未満である必要があります。Tangemは、この条件を満たす十分な残高が貯まった時点で、Aaveへの資金移動を行います。 + 取引手数料は入金額の4%未満である必要があります。残高がこの条件を満たすのに十分な金額になった場合にのみ、TangemはAaveに資金を送ります。 最低入金額 手数料ポリシー - Tangemはまた、得られた利回りに対して3%のサービス手数料を差し引きます。 + Tangemはまた、得られた利回りに対して3%のサービス手数料を差し引きます。 ネットワーク手数料が現在高すぎます。設定した上限を下回るまで待機しています。 過去のリターン ここに説明を入力してください。1〜3行が理想的です。[プレースホルダー] @@ -1748,7 +1773,7 @@ 分散型・自己管理型 サービスを利用することにより、プロバイダー\n %1$sおよび%2$sに同意したことになります 年間%s%%の収益 - Aave • 変動金利 + Aave %1$s%% • 変動金利 Aave 平均%s 昨年のリターン diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2158343988..48aa70401e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -542,8 +542,12 @@ If you do, you\'ll need to start over. Recover existing wallet via Google Drive backup Google Drive backup + Create a new secure wallet and transfer your funds for extra protection. + Create new wallet Level up your security with the superior Tangem hardware wallet. Hardware Wallet + Move your current wallet into Tangem Wallet. + Upgrade current wallet Go to backup Please back up your wallet before creating an access code. Finalize backup first @@ -1813,6 +1817,8 @@ APY %1$s%% Available Current APY + When topping up for lending, a network fee will be deducted from the amount — never more than %1$s + The network fee is currently too high to execute lending. Funds will be supplied once it drops to %1$s or below. My funds Your %1$s is now deposited in Aave and earning interest. You hold a%2$s token, which represents your balance and grows over time. When you top up, funds go to Aave to earn interest, minus a transaction fee. Earn @@ -1824,7 +1830,7 @@ All future %s top-ups will be supplied to Aave automatically, with the transaction fee deducted. If network fees rise above maximum fee, the transaction won’t go through until they decrease. You can change this limit later. Maximum fee - The transaction fee must stay below 4% of your deposit. Tangem will transfer funds to Aave only once your balance is large enough to meet this condition. + The transaction fee must stay below 4% of your deposit. Tangem will transfer funds to Aave only once your balance is large enough to meet this condition. Minimal top-up Fee policy Tangem also takes a 3% service fee on the yield earned. diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt new file mode 100644 index 0000000000..4032d39149 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/FeeExtensions.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.yield.supply + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.models.currency.CryptoCurrency +import java.math.BigInteger + +fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger): Fee = 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 +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyConst.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyConst.kt new file mode 100644 index 0000000000..f4ae8729f3 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyConst.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.yield.supply + +object YieldSupplyConst { + val YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT = 350_000.toBigInteger() +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt index 273114f51c..58f1a8476f 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt @@ -3,17 +3,16 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.yield.supply.fixFee import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.transaction.error.GetFeeError import com.tangem.utils.extensions.isSingleItem import timber.log.Timber -import java.math.BigInteger class YieldSupplyEstimateEnterFeeUseCase( private val feeRepository: FeeRepository, @@ -107,24 +106,6 @@ class YieldSupplyEstimateEnterFeeUseCase( return withCalculatedFees + withEstimatedFees } - 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 { // Using constant gas limit to avoid fee calculation errors when contract address is not deployed yet val ETHEREUM_CONSTANT_GAS_LIMIT = 500_000.toBigInteger() diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt new file mode 100644 index 0000000000..c9ee2d6a93 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt @@ -0,0 +1,65 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import arrow.core.Either.Companion.catch +import com.tangem.domain.yield.supply.fixFee +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 com.tangem.domain.yield.supply.YieldSupplyConst.YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Calculates current fee for Yield Supply enter transaction expressed in token units. + */ +class YieldSupplyGetCurrentFeeUseCase( + 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, + YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT, + ) + + val rateRatio = nativeFiatRate.divide( + fiatRate, + cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + + val tokenValue = rateRatio.multiply(nativeGas.amount.value) + + tokenValue.stripTrailingZeros() + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetMaxFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetMaxFeeUseCase.kt new file mode 100644 index 0000000000..e463368cd3 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetMaxFeeUseCase.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import arrow.core.Either.Companion.catch +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.yield.supply.YieldSupplyRepository +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Calculates max allowed network fee for Yield Supply enter transaction expressed in token units. + * + * Uses YieldMarketToken.maxFeeNative (native coin units) and converts it to token units with the + * same conversion logic as [YieldSupplyGetCurrentFeeUseCase]: based on fiat rate ratio + * (nativeFiatRate / tokenFiatRate). + */ +class YieldSupplyGetMaxFeeUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, + private val quotesRepository: QuotesRepository, + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Either = catch { + val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + ?: error("CryptoCurrency must be token for max fee calculation") + + 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 marketToken = yieldSupplyRepository.getTokenStatus(token) + val maxFeeNative = marketToken.maxFeeNative.toBigDecimal() + + val rateRatio = nativeFiatRate.divide( + fiatRate, + cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + + val tokenValue = rateRatio.multiply(maxFeeNative) + + tokenValue.stripTrailingZeros() + } +} \ No newline at end of file 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 index b544b7fe62..9e6f8b109d 100644 --- 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 @@ -2,16 +2,15 @@ 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.yield.supply.fixFee 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 com.tangem.domain.yield.supply.YieldSupplyConst.YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT import java.math.BigDecimal -import java.math.BigInteger import java.math.RoundingMode class YieldSupplyMinAmountUseCase( @@ -45,7 +44,10 @@ class YieldSupplyMinAmountUseCase( ?: 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 nativeGas = feeWithoutGas.fixFee( + nativeCryptoCurrency, + YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT, + ) val rateRatio = nativeFiatRate.divide( fiatRate, @@ -62,27 +64,8 @@ class YieldSupplyMinAmountUseCase( .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/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/YieldSupplyAmountFormatter.kt similarity index 53% rename from features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyMinAmountFormatter.kt rename to features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/formatter/YieldSupplyAmountFormatter.kt index 08c48d9cb0..296cd74afb 100644 --- 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/YieldSupplyAmountFormatter.kt @@ -2,24 +2,38 @@ package com.tangem.features.yield.supply.impl.common.formatter import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.approximateAmount 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 com.tangem.utils.StringsSigns.DOT import java.math.BigDecimal -internal class YieldSupplyMinAmountFormatter( +internal class YieldSupplyAmountFormatter( private val feeCryptoCurrency: CryptoCurrency, private val appCurrency: AppCurrency, ) { - operator fun invoke(feeValue: BigDecimal, fiatRate: BigDecimal?): TextReference { + operator fun invoke(feeValue: BigDecimal, fiatRate: BigDecimal?, showCrypto: Boolean = true): TextReference { val cryptoFee = feeValue.format { crypto(feeCryptoCurrency) } val fiatFeeValue = fiatRate?.let(feeValue::multiply) - val fiatFee = fiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) } + val fiatFee = if (showCrypto) { + fiatFeeValue.format { + fiat(appCurrency.code, appCurrency.symbol) + .approximateAmount() + } + } else { + fiatFeeValue.format { + fiat(appCurrency.code, appCurrency.symbol) + } + } - return stringReference(cryptoFee + " ${StringsSigns.DOT} " + fiatFee) + return if (showCrypto) { + stringReference("$cryptoFee $DOT $fiatFee") + } else { + 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/YieldSupplyFeeRow.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyFeeRow.kt index 2f2901b79d..dc00a547a8 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyFeeRow.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/ui/YieldSupplyFeeRow.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TextShimmer @@ -46,6 +47,7 @@ internal fun YieldSupplyFeeRow(title: TextReference, value: TextReference?) { text = targetValue.resolveReference(), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, ) } else { TextShimmer( 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 7b1e55f50a..9b93df6c2a 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 @@ -10,6 +10,9 @@ internal data class YieldSupplyActiveContentUM( val subtitle: TextReference, val subtitleLink: TextReference, val notificationUM: NotificationUM?, - val apy: TextReference? = null, val minAmount: TextReference?, + val currentFee: TextReference?, + val feeDescription: TextReference?, + val apy: TextReference? = null, + val isHighFee: Boolean = false, ) \ 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 2b63d7405c..1b9157aa2e 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 @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference 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.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -19,9 +20,11 @@ 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.domain.yield.supply.usecase.YieldSupplyGetCurrentFeeUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetMaxFeeUseCase import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics 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.common.formatter.YieldSupplyAmountFormatter 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 @@ -40,6 +43,8 @@ internal class YieldSupplyActiveModel @Inject constructor( private val yieldSupplyGetProtocolBalanceUseCase: YieldSupplyGetProtocolBalanceUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, + private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase, + private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { @@ -62,6 +67,9 @@ internal class YieldSupplyActiveModel @Inject constructor( subtitleLink = resourceReference(R.string.common_read_more), notificationUM = null, minAmount = null, + currentFee = null, + feeDescription = null, + isHighFee = false, ), ) @@ -115,6 +123,7 @@ internal class YieldSupplyActiveModel @Inject constructor( loadApy() loadMinAmount() + loadFees() uiState.update { it.copy( @@ -154,10 +163,14 @@ internal class YieldSupplyActiveModel @Inject constructor( params.userWallet, cryptoCurrencyStatusFlow.value, ).onRight { minAmount -> - val minAmountTextReference = YieldSupplyMinAmountFormatter( + val minAmountTextReference = YieldSupplyAmountFormatter( cryptoCurrencyStatusFlow.value.currency, appCurrency, - ).invoke(minAmount, cryptoCurrencyStatusFlow.value.value.fiatRate) + ).invoke( + feeValue = minAmount, + fiatRate = cryptoCurrencyStatusFlow.value.value.fiatRate, + showCrypto = false, + ) uiState.update { it.copy(minAmount = minAmountTextReference) } @@ -169,6 +182,51 @@ internal class YieldSupplyActiveModel @Inject constructor( } } + private fun loadFees() { + modelScope.launch(dispatchers.default) { + val cryptoStatus = cryptoCurrencyStatusFlow.value + + val currentFee = yieldSupplyGetCurrentFeeUseCase( + userWallet = params.userWallet, + cryptoCurrencyStatus = cryptoStatus, + ).getOrNull() + + val maxFee = yieldSupplyGetMaxFeeUseCase( + userWallet = params.userWallet, + cryptoCurrencyStatus = cryptoStatus, + ).getOrNull() + + val currentFeeText = currentFee?.let { + YieldSupplyAmountFormatter( + cryptoStatus.currency, + appCurrency, + ).invoke( + feeValue = it, + fiatRate = cryptoStatus.value.fiatRate, + showCrypto = false, + ) + } + + val isHighFee = if (currentFee != null && maxFee != null) currentFee > maxFee else false + + val maxFiatFee = cryptoStatus.value.fiatRate?.multiply(maxFee) + .format { fiat(appCurrency.code, appCurrency.symbol) } + val feeDescription = if (isHighFee) { + resourceReference(R.string.yield_module_earn_sheet_high_fee_description, wrappedList(maxFiatFee)) + } else { + resourceReference(R.string.yield_module_earn_sheet_fee_description, wrappedList(maxFiatFee)) + } + + uiState.update { + it.copy( + currentFee = currentFeeText ?: stringReference(DASH_SIGN), + isHighFee = isHighFee, + feeDescription = feeDescription, + ) + } + } + } + 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 a20183fb10..9e2346fe2f 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 @@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withLink import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -70,6 +71,22 @@ internal fun YieldSupplyActiveContent( } YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden) + + AnimatedVisibility(state.feeDescription != null) { + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = state.feeDescription?.resolveReference().orEmpty(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = stringResourceSafe(R.string.yield_module_fee_policy_sheet_min_amount_note), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) } } @@ -99,7 +116,9 @@ private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) { painterResource(R.drawable.ic_arrow_up_8), tint = TangemTheme.colors.text.accent, contentDescription = null, - modifier = Modifier.padding(end = 6.dp).size(12.dp), + modifier = Modifier + .padding(end = 6.dp) + .size(12.dp), ) Text( modifier = modifier, @@ -113,6 +132,7 @@ private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) { } } +@Suppress("LongMethod") @Composable private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanceHidden: Boolean) { Column( @@ -173,6 +193,15 @@ private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanc info = state.minAmount, isBalanceHidden = false, ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + HighComissionInfoRow( + title = resourceReference(R.string.common_network_fee_title), + info = state.currentFee, + isHighComission = state.isHighFee, + ) } } @@ -220,6 +249,7 @@ private fun InfoRow(title: TextReference, isBalanceHidden: Boolean, info: TextRe text = currentInfo.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, ) } else { TextShimmer( @@ -231,6 +261,57 @@ private fun InfoRow(title: TextReference, isBalanceHidden: Boolean, info: TextRe } } +@Composable +private fun HighComissionInfoRow(title: TextReference, info: TextReference?, isHighComission: Boolean) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 12.dp), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + + AnimatedContent(info) { currentInfo -> + if (currentInfo != null) { + if (isHighComission) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + painterResource(R.drawable.ic_token_info_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = TangemTheme.colors.text.warning, + ) + Text( + text = currentInfo.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.warning, + textAlign = TextAlign.End, + ) + } + } else { + Text( + text = currentInfo.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + } else { + TextShimmer( + text = title.resolveReference(), + style = TangemTheme.typography.body1, + ) + } + } + } +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) @@ -262,6 +343,12 @@ private class YieldSupplyActiveBottomSheetPreviewProvider : PreviewParameterProv notificationUM = NotificationUM.Error.InvalidAmount, apy = stringReference("5,14%"), minAmount = stringReference("50 USDT"), + isHighFee = true, + feeDescription = stringReference( + "The network fee is currently too high to execute lending." + + "Funds will be supplied once it drops to \$12 or below. ", + ), + currentFee = stringReference("30 USDT"), ), ) } 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 5e0ba9a4a9..51e5126eb7 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,7 +10,7 @@ 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.features.yield.supply.impl.common.formatter.YieldSupplyAmountFormatter import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList @@ -49,7 +49,7 @@ internal class YieldSupplyStartEarningFeeContentTransformer( } val maxFiatFee = maxFiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) } - val minAmountTextReference = YieldSupplyMinAmountFormatter( + val minAmountTextReference = YieldSupplyAmountFormatter( cryptoCurrency, appCurrency, ).invoke(minAmount, cryptoCurrencyStatus.value.fiatRate)