Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-20 13:01:49 +05:00
parent 92c95095cc
commit fbfd7476fa
20 changed files with 430 additions and 66 deletions

View file

@ -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,
)
}
}

View file

@ -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")
}
}

View file

@ -21,4 +21,5 @@ data class EnvironmentConfig(
val tangemApiKeyDev: String? = null,
val tangemApiKeyStage: String? = null,
val yieldModuleApiKey: String? = null,
val yieldModuleApiKeyDev: String? = null,
)

View file

@ -30,6 +30,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
tangemApiKeyDev = value.tangemApiKeyDev,
tangemApiKeyStage = value.tangemApiKeyStage,
yieldModuleApiKey = value.yieldModuleApiKey,
yieldModuleApiKeyDev = value.yieldModuleApiKeyDev,
)
}
}

View file

@ -45,6 +45,7 @@ class EnvironmentConfigModel(
@Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?,
@Json(name = "etherscanApiKey") val etherScanApiKey: String?,
@Json(name = "yieldModuleApiKey") val yieldModuleApiKey: String?,
@Json(name = "yieldModuleApiKeyDev") val yieldModuleApiKeyDev: String?,
@Json(name = "blinkApiKey") val blinkApiKey: String?,
@Json(name = "tatumApiKey") val tatumApiKey: String?,
)

View file

@ -20,6 +20,7 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
tangemApiKeyDev = TANGEM_API_KEY_DEV,
tangemApiKeyStage = TANGEM_API_KEY_STAGE,
yieldModuleApiKey = YIELD_MODULE_KEY,
yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV,
)
override suspend fun initialize() = environmentConfig
@ -34,5 +35,6 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
const val TANGEM_API_KEY_DEV = "tangem_api_key_dev"
const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage"
const val YIELD_MODULE_KEY = "yield_module_api_key"
const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev"
}
}

View file

@ -22,6 +22,7 @@
<string name="account_archived_recover_error_message">すでにアクティブアカウントの上限20件に達しています。復元するには、1つをアーカイブしてください。</string>
<string name="account_archived_recover_error_title">アカウントを復元できません</string>
<string name="account_archived_title">アーカイブ済み</string>
<string name="account_could_not_archive">アカウントをアーカイブできませんでした。しばらくしてからもう一度お試しください。</string>
<string name="account_could_not_create">アカウントを作成できませんでした。しばらくしてからもう一度お試しください。</string>
<string name="account_create_success_message">アカウントを作成しました</string>
<string name="account_details_archive">アカウントをアーカイブする</string>
@ -249,10 +250,12 @@
<string name="common_fee_selector_option_slow">遅い</string>
<string name="common_fee_selector_title">速度と料金</string>
<string name="common_finish">終了</string>
<string name="common_forget">忘れる</string>
<string name="common_free">無料</string>
<string name="common_from">送信元</string>
<string name="common_generate_addresses">アドレスを同期する</string>
<string name="common_get_started">はじめる</string>
<string name="common_get_token">トークンを取得</string>
<string name="common_go_to_provider">プロバイダーへ移動</string>
<string name="common_go_to_token">トークンへ移動</string>
<string name="common_got_it">わかりました</string>
@ -526,14 +529,20 @@
<string name="hw_activation_need_warning_description">アクセスコードでアプリを保護して、設定を完了してください。</string>
<string name="hw_backup_alert_description">そうした場合は、最初からやり直す必要があります。</string>
<string name="hw_backup_alert_title">本当にアクティベーション処理を終了してもよろしいですか?</string>
<string name="hw_backup_banner_description">暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。</string>
<string name="hw_backup_close_description">実行すると、最初からやり直す必要があります。</string>
<string name="hw_backup_google_drive_description">Googleドライブのバックアップから既存のウォレットを復元する</string>
<string name="hw_backup_google_drive_title">Googleドライブのバックアップ</string>
<string name="hw_backup_hardware_create_description">さらに強固なセキュリティのために、新しいウォレットを作成して資産を移動しましょう。</string>
<string name="hw_backup_hardware_create_title">新しいウォレットを作成</string>
<string name="hw_backup_hardware_description">Tangemの高性能ハードウェアウォレットで、セキュリティをさらに強化しましょう。</string>
<string name="hw_backup_hardware_title">ハードウェアウォレット</string>
<string name="hw_backup_hardware_upgrade_description">現在のウォレットをTangemウォレットに移します。</string>
<string name="hw_backup_hardware_upgrade_title">現在のウォレットをアップグレードする</string>
<string name="hw_backup_need_action">バックアップへ移動</string>
<string name="hw_backup_need_description">アクセスコードを作成する前にウォレットをバックアップしてください。</string>
<string name="hw_backup_need_title">まずバックアップを完了する</string>
<string name="hw_backup_section_other_title">その他の方法</string>
<string name="hw_backup_seed_description">秘密鍵をオフラインで安全に保存する物理デバイス。</string>
<string name="hw_backup_seed_title">リカバリーフレーズ</string>
<string name="hw_create_keys_title">鍵はアプリに保存されます</string>
@ -603,6 +612,7 @@
<string name="manage_tokens_unavailable_description">選択したトークンは現在、暗号資産ウォレット内でのアクションには利用できません。しかし、心配しないでください。賛成票を投じることで関心を表明できます。</string>
<string name="manage_tokens_unavailable_vote">賛成票を投じる</string>
<string name="manage_tokens_wallet_support_only_one_network_title">ウォレットは複数のネットワークをサポートしていません。</string>
<string name="markets_about_coin_header">コインについて</string>
<string name="markets_add_to_my_portfolio_description">このアセットを購入・交換・受け取るには、ポートフォリオに追加してください。</string>
<string name="markets_add_to_my_portfolio_unavailable_description">このアセットは現在ウォレットで利用できません</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">このアセットはこのウォレットでは使用できません。</string>
@ -638,6 +648,7 @@
<string name="markets_sort_by_trending_title">トレンド</string>
<string name="markets_staking_banner_description_placeholder">ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s</string>
<string name="markets_staking_banner_title">最大%s APYを獲得</string>
<string name="markets_token_added">トークンを追加しました</string>
<string name="markets_token_details_about_token_title">%sについて</string>
<plurals name="markets_token_details_amount_exchanges">
<item quantity="other">%d取引所</item>
@ -895,7 +906,7 @@
<item quantity="other">最大%d日</item>
</plurals>
<string name="onramp_timing_minutes">%s分</string>
<string name="onramp_title_available_from">下記より利用可能</string>
<string name="onramp_title_available_from">利用可能</string>
<string name="onramp_title_you_get">以下が手に入ります。</string>
<string name="onramp_tos_external_providers">サービスは外部プロバイダーによって提供されます。 \nTangemは責任を負いません。</string>
<string name="onramp_transaction_status_footer_text">この画面を閉じて、トークンの詳細画面で取引状況を確認できます。</string>
@ -1275,7 +1286,20 @@
<string name="swapping_to_title">受け取る</string>
<string name="swapping_token_list_title">トークンを選択</string>
<string name="swapping_token_not_available">利用不可</string>
<string name="tangem_pay_deposit">入金</string>
<string name="tangem_pay_dispute">異議申し立て</string>
<string name="tangem_pay_explore_transaction">取引を表示</string>
<string name="tangem_pay_fee_subtitle">サービス手数料</string>
<string name="tangem_pay_fee_title">手数料</string>
<string name="tangem_pay_status_completed">完了</string>
<string name="tangem_pay_status_declined">拒否</string>
<string name="tangem_pay_status_pending">保留中</string>
<string name="tangem_pay_transaction_declined_notification_text">銀行がこの取引リクエストを拒否しました。</string>
<string name="tangem_pay_transaction_fee_notification_text">この手数料は、送金処理にかかるコストをカバーするためのものです。</string>
<string name="tangem_pay_withdrawal">出金</string>
<string name="tangempay_card_details_change_pin">PINを変更する</string>
<string name="tangempay_card_details_error_text">データの読み込みに失敗しました。しばらくしてからもう一度お試しください。</string>
<string name="tangempay_card_details_freeze_card">カードの一時停止</string>
<string name="tangempay_card_details_hide_text">非表示</string>
<string name="tangempay_card_details_receive_error_description">技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。</string>
<string name="tangempay_card_details_receive_error_title">現在、受け取りは利用できません</string>
@ -1310,6 +1334,7 @@
<string name="token_button_unavailability_reason_pending_transaction_send">ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。</string>
<string name="token_button_unavailability_reason_sell_unavailable">%sの売却は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。</string>
<string name="token_button_unavailability_reason_staking_unavailable">%sのステーキングは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。</string>
<string name="token_button_unavailability_reason_yield_supply_approval">ここにテキストを入力</string>
<string name="token_details_generate_xpub">XPUBを生成する</string>
<string name="token_details_hide_alert_hide">非表示</string>
<string name="token_details_hide_alert_message">このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。</string>
@ -1729,10 +1754,10 @@
<string name="yield_module_fee_policy_sheet_description">今後の%sの入金はすべて、取引手数料が差し引かれて自動的にAaveに供給されます。</string>
<string name="yield_module_fee_policy_sheet_max_fee_note">ネットワーク手数料が上限手数料を超えた場合、手数料が下がるまで取引は成立しません。この制限は後で変更できます。</string>
<string name="yield_module_fee_policy_sheet_max_fee_title">最大手数料</string>
<string name="yield_module_fee_policy_sheet_min_amount_note">取引手数料は、預入額の4未満である必要があります。Tangemは、この条件を満たす十分な残高が貯まった時点で、Aaveへの資金移動を行います。</string>
<string name="yield_module_fee_policy_sheet_min_amount_note">取引手数料は入金額の4%未満である必要があります。残高がこの条件を満たすのに十分な金額になった場合にのみ、TangemはAaveに資金を送ります。</string>
<string name="yield_module_fee_policy_sheet_min_amount_title">最低入金額</string>
<string name="yield_module_fee_policy_sheet_title">手数料ポリシー</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangemはまた、得られた利回りに対して3のサービス手数料を差し引きます。</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangemはまた、得られた利回りに対して3%のサービス手数料を差し引きます。</string>
<string name="yield_module_high_fee_error">ネットワーク手数料が現在高すぎます。設定した上限を下回るまで待機しています。</string>
<string name="yield_module_historical_returns">過去のリターン</string>
<string name="yield_module_main_view_approve_notification_description">ここに説明を入力してください。1〜3行が理想的です。[プレースホルダー]</string>
@ -1748,7 +1773,7 @@
<string name="yield_module_promo_screen_self_custodial_title">分散型・自己管理型</string>
<string name="yield_module_promo_screen_terms_disclaimer">サービスを利用することにより、プロバイダー\n %1$sおよび%2$sに同意したことになります</string>
<string name="yield_module_promo_screen_title">年間%s%%の収益</string>
<string name="yield_module_promo_screen_variable_rate_info">Aave • 変動金利</string>
<string name="yield_module_promo_screen_variable_rate_info">Aave %1$s%% • 変動金利</string>
<string name="yield_module_provider">Aave</string>
<string name="yield_module_rate_info_sheet_chart_average">平均%s</string>
<string name="yield_module_rate_info_sheet_chart_title">昨年のリターン</string>

View file

@ -542,8 +542,12 @@
<string name="hw_backup_close_description">If you do, you\'ll need to start over.</string>
<string name="hw_backup_google_drive_description">Recover existing wallet via Google Drive backup</string>
<string name="hw_backup_google_drive_title">Google Drive backup</string>
<string name="hw_backup_hardware_create_description">Create a new secure wallet and transfer your funds for extra protection.</string>
<string name="hw_backup_hardware_create_title">Create new wallet</string>
<string name="hw_backup_hardware_description">Level up your security with the superior Tangem hardware wallet.</string>
<string name="hw_backup_hardware_title">Hardware Wallet</string>
<string name="hw_backup_hardware_upgrade_description">Move your current wallet into Tangem Wallet.</string>
<string name="hw_backup_hardware_upgrade_title">Upgrade current wallet</string>
<string name="hw_backup_need_action">Go to backup</string>
<string name="hw_backup_need_description">Please back up your wallet before creating an access code.</string>
<string name="hw_backup_need_title">Finalize backup first</string>
@ -1813,6 +1817,8 @@
<string name="yield_module_earn_badge">APY %1$s%%</string>
<string name="yield_module_earn_sheet_available_title">Available</string>
<string name="yield_module_earn_sheet_current_apy_title">Current APY</string>
<string name="yield_module_earn_sheet_fee_description">When topping up for lending, a network fee will be deducted from the amount — never more than %1$s</string>
<string name="yield_module_earn_sheet_high_fee_description">The network fee is currently too high to execute lending. Funds will be supplied once it drops to %1$s or below. </string>
<string name="yield_module_earn_sheet_my_funds_title">My funds</string>
<string name="yield_module_earn_sheet_provider_description">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.</string>
<string name="yield_module_earn_sheet_title">Earn</string>
@ -1824,7 +1830,7 @@
<string name="yield_module_fee_policy_sheet_description">All future %s top-ups will be supplied to Aave automatically, with the transaction fee deducted.</string>
<string name="yield_module_fee_policy_sheet_max_fee_note">If network fees rise above maximum fee, the transaction wont go through until they decrease. You can change this limit later.</string>
<string name="yield_module_fee_policy_sheet_max_fee_title">Maximum fee</string>
<string name="yield_module_fee_policy_sheet_min_amount_note">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. </string>
<string name="yield_module_fee_policy_sheet_min_amount_note">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.</string>
<string name="yield_module_fee_policy_sheet_min_amount_title">Minimal top-up</string>
<string name="yield_module_fee_policy_sheet_title">Fee policy</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem also takes a 3% service fee on the yield earned.</string>

View file

@ -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
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.yield.supply
object YieldSupplyConst {
val YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT = 350_000.toBigInteger()
}

View file

@ -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()

View file

@ -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<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,
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()
}
}

View file

@ -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<Throwable, BigDecimal> = 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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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(

View file

@ -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,
)

View file

@ -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"
}

View file

@ -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"),
),
)
}

View file

@ -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)