Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-18 10:31:03 +02:00
parent 9e0b8b3234
commit b449775ad8
112 changed files with 979 additions and 740 deletions

View file

@ -16,7 +16,7 @@ dependencies {
/** Domain models */
api(projects.domain.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.api
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
@ -10,7 +11,7 @@ interface StakingComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val yieldId: String,
val integrationId: StakingIntegrationID,
)
interface Factory : ComponentFactory<Params, StakingComponent>

View file

@ -18,10 +18,10 @@ internal class StakingAnalyticSender(
fun initialInfoScreen(value: StakingUiState) {
val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data
val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
val validatorCount = validatorState?.balances
?.filterNot { it.validator?.address.isNullOrBlank() }
?.distinctBy { it.validator?.address }
val balanceState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
val validatorCount = balanceState?.balances
?.filterNot { it.target?.address.isNullOrBlank() }
?.distinctBy { it.target?.address }
?.size ?: 0
analyticsEventHandler.send(
@ -34,7 +34,7 @@ internal class StakingAnalyticSender(
fun confirmationScreen(value: StakingUiState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
@ -80,7 +80,7 @@ internal class StakingAnalyticSender(
fun sendTransactionStakingAnalytics(value: StakingUiState, cryptoCurrencyStatus: CryptoCurrencyStatus) {
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
analyticsEventHandler.send(
Basic.TransactionSent(
@ -102,7 +102,7 @@ internal class StakingAnalyticSender(
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
analyticsEventHandler.send(
StakingAnalyticsEvent.ButtonAction(

View file

@ -1,6 +1,5 @@
package com.tangem.features.staking.impl.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
@ -8,7 +7,6 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
@ -28,7 +26,6 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
private val appRouter: AppRouter,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val getYieldUseCase: GetYieldUseCase,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
) : StakingDeepLinkHandler {
@ -73,19 +70,13 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
return@launch
}
val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync(
val availability = getStakingAvailabilityUseCase.invokeSync(
userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency,
).getOrNull()
if (isStakingEnabled !is StakingAvailability.Available) {
return@launch
}
val yield = getYieldUseCase.invoke(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
).getOrElse {
val option = (availability as? StakingAvailability.Available)?.option
if (option == null) {
Timber.e("Staking is unavailable for ${cryptoCurrency.name}")
return@launch
}
@ -94,7 +85,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
AppRoute.Staking(
userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency,
yieldId = yield.id,
integrationId = option.integrationId,
),
)
}

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import java.math.BigDecimal
@ -35,7 +35,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openValidators()
fun onValidatorSelect(validator: Yield.Validator)
fun onTargetSelect(target: StakingTarget)
fun openRewardsValidators()

View file

@ -45,11 +45,15 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.*
import com.tangem.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.domain.staking.model.P2PEthPoolIntegration
import com.tangem.domain.staking.model.StakeKitIntegration
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.utils.getValidatorsCount
@ -134,6 +138,7 @@ internal class StakingModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getActionsUseCase: GetActionsUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
@ -164,9 +169,18 @@ internal class StakingModel @Inject constructor(
private val cryptoCurrencyId: CryptoCurrency.ID = params.cryptoCurrency.id
private val userWalletId: UserWalletId = params.userWalletId
private val yield: Yield = runBlocking {
getYieldUseCase(params.yieldId).getOrElse {
error("yield must be not null")
private val integration: StakingIntegration = runBlocking {
when (val integrationId = params.integrationId) {
is StakingIntegrationID.StakeKit -> {
val yield = getYieldUseCase(integrationId.value).getOrElse {
error("yield must be not null")
}
StakeKitIntegration(integrationId, yield)
}
StakingIntegrationID.P2PEthPool -> {
val vaults = p2pEthPoolRepository.getVaults().getOrElse { emptyList() }
P2PEthPoolIntegration(integrationId, vaults)
}
}
}
@ -194,7 +208,7 @@ internal class StakingModel @Inject constructor(
return invalidatePendingTransactionsUseCase(
balanceItems = stakeKitBalance?.balance?.items.orEmpty(),
stakingActions = stakingActions,
token = yield.token,
token = integration.token,
).getOrElse { emptyList() }
}
@ -205,7 +219,7 @@ internal class StakingModel @Inject constructor(
stakingBalanceUpdater.create(
cryptoCurrencyStatus,
userWallet,
yield,
integration,
)
}
@ -213,7 +227,7 @@ internal class StakingModel @Inject constructor(
stakingFeeTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet,
yield = yield,
integration = integration,
)
}
@ -221,7 +235,7 @@ internal class StakingModel @Inject constructor(
stakingTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet,
yield = yield,
integration = integration,
isAmountSubtractAvailable = isAmountSubtractAvailable,
)
}
@ -280,7 +294,7 @@ internal class StakingModel @Inject constructor(
val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit
when {
isInitialInfoStep && noBalanceState && yield.allValidatorsFull && hasNoYieldBalanceData -> {
isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> {
stakingEventFactory.createStakingValidatorsUnavailableAlert()
return@launch
}
@ -293,12 +307,12 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingApproval = stakingApproval,
stakingAllowance = stakingAllowance,
yieldArgs = yield.args,
integration = integration,
).let(::add)
if (yield.args.enter.isPartialAmountDisabled) {
if (integration.isPartialAmountDisabled) {
ValidatorSelectChangeTransformer(
selectedValidator = yield.preferredValidators.firstOrNull(),
yield = yield,
selectedTarget = integration.preferredTargets.firstOrNull(),
integration = integration,
).let(::add)
SetAmountDataTransformer(
clickIntents = this@StakingModel,
@ -313,7 +327,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
yield = yield,
integration = integration,
).let(::add)
}
}
@ -327,7 +341,7 @@ internal class StakingModel @Inject constructor(
override fun getFee() {
stateController.update(
SetConfirmationStateLoadingTransformer(
yield = yield,
integration = integration,
appCurrency = appCurrency,
cryptoCurrency = cryptoCurrencyStatus.currency,
),
@ -479,7 +493,7 @@ internal class StakingModel @Inject constructor(
}
override fun onAmountEnterClick() {
if (yield.preferredValidators.isEmpty()) {
if (integration.preferredTargets.isEmpty()) {
stateController.updateEvent(
StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators),
)
@ -487,8 +501,8 @@ internal class StakingModel @Inject constructor(
if (uiState.value.actionType is StakingActionCommonType.Enter) {
stateController.updateAll(
ValidatorSelectChangeTransformer(
selectedValidator = null,
yield = yield,
selectedTarget = null,
integration = integration,
),
)
}
@ -502,7 +516,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
value = value,
yield = yield,
integration = integration,
),
)
}
@ -518,7 +532,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
yield = yield,
integration = integration,
),
)
}
@ -539,16 +553,16 @@ internal class StakingModel @Inject constructor(
stakingStateRouter.showValidators()
}
override fun onValidatorSelect(validator: Yield.Validator) {
override fun onTargetSelect(target: StakingTarget) {
analyticsEventHandler.send(
StakingAnalyticsEvent.ValidatorChosen(
validator = validator.name,
validator = target.name,
),
)
stateController.update(
ValidatorSelectChangeTransformer(
selectedValidator = validator,
yield = yield,
selectedTarget = target,
integration = integration,
),
)
}
@ -606,7 +620,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type,
pendingActions = activeStake.pendingActions,
balanceState = activeStake,
validator = activeStake.validator,
target = activeStake.target,
amountValue = activeStake.cryptoValue,
)
onNextClick(activeStake)
@ -619,7 +633,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type,
pendingAction = action,
balanceState = activeStake,
validator = activeStake.validator,
target = activeStake.target,
amountValue = activeStake.cryptoValue,
)
stateController.update(DismissBottomSheetStateTransformer)
@ -788,7 +802,7 @@ internal class StakingModel @Inject constructor(
isSubtractAvailable = isAmountSubtractAvailable,
feeError = feeError,
stakingError = stakingError,
yield = yield,
integration = integration,
),
)
}
@ -911,7 +925,7 @@ internal class StakingModel @Inject constructor(
val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data
val feeState = confirmationState?.feeState as? FeeState.Content
val validator = validatorState?.chosenValidator
val target = validatorState?.chosenTarget
val feeAmount = feeState?.fee?.amount
val amount = amountState?.amountTextField?.cryptoAmount
saveBlockchainErrorUseCase(
@ -919,7 +933,7 @@ internal class StakingModel @Inject constructor(
errorMessage = errorMessage,
blockchainId = network.rawId,
derivationPath = network.derivationPath.value,
destinationAddress = validator?.address.orEmpty(),
destinationAddress = target?.address.orEmpty(),
tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol,
amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
fee = feeAmount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
@ -928,7 +942,7 @@ internal class StakingModel @Inject constructor(
val email = FeedbackEmailType.StakingProblem(
walletMetaInfo = metaInfo,
validatorName = validator?.name,
validatorName = target?.name,
transactionTypes = transactionsInProgress.map { it.type.name },
unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction },
)
@ -1229,7 +1243,7 @@ internal class StakingModel @Inject constructor(
stateController.updateAll(
SetInitialDataStateTransformer(
clickIntents = this@StakingModel,
yield = yield,
integration = integration,
isAnyTokenStaked = isAnyTokenStaked,
cryptoCurrencyStatus = status,
userWalletProvider = Provider { userWallet },
@ -1248,7 +1262,7 @@ internal class StakingModel @Inject constructor(
balanceState: BalanceState,
pendingActions: ImmutableList<PendingAction> = persistentListOf(),
pendingAction: PendingAction? = pendingActions.firstOrNull(),
validator: Yield.Validator?,
target: StakingTarget?,
amountValue: String,
) {
stateController.updateAll(
@ -1261,11 +1275,11 @@ internal class StakingModel @Inject constructor(
pendingActions = pendingActions,
pendingAction = pendingAction,
stakingAllowance = stakingAllowance,
yieldArgs = yield.args,
integration = integration,
),
ValidatorSelectChangeTransformer(
selectedValidator = validator,
yield = yield,
selectedTarget = target,
integration = integration,
),
SetAmountDataTransformer(
clickIntents = this,
@ -1280,7 +1294,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
value = amountValue,
minimumTransactionAmount = minimumTransactionAmount,
yield = yield,
integration = integration,
),
)
}

View file

@ -6,7 +6,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.PendingActionConstraints
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.StakingTarget
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@ -35,10 +35,10 @@ internal data class BalanceState(
val fiatAmount: BigDecimal?,
val formattedFiatAmount: TextReference,
val rawCurrencyId: String?,
val validator: Yield.Validator?,
val target: StakingTarget?,
val pendingActions: ImmutableList<PendingAction>,
val isPending: Boolean,
val validatorAddress: String?,
val targetAddress: String?,
)
@Immutable

View file

@ -10,7 +10,7 @@ import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
@ -97,9 +97,9 @@ internal sealed class StakingStates {
override val isPrimaryButtonEnabled: Boolean,
override val isClickable: Boolean,
val isVisibleOnConfirmation: Boolean,
val chosenValidator: Yield.Validator,
val activeValidator: Yield.Validator?,
val availableValidators: List<Yield.Validator>,
val chosenTarget: StakingTarget,
val activeTarget: StakingTarget?,
val availableTargets: List<StakingTarget>,
) : ValidatorState()
data class Empty(

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.BalanceType.Companion.isClickable
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -29,24 +29,24 @@ import java.util.Calendar
internal class BalanceItemConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<BalanceItem, BalanceState?> {
override fun convert(value: BalanceItem): BalanceState? {
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val validator = yield.validators.firstOrNull {
val target = integration.targets.firstOrNull {
value.validatorAddress?.contains(it.address, ignoreCase = true) == true
}
val cryptoAmount = value.getBalanceValue()
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
val title = value.type.getTitle(validator?.name)
val title = value.type.getTitle(target?.name)
return title?.let {
BalanceState(
groupId = value.groupId,
validator = validator,
target = target,
title = title,
subtitle = getSubtitle(value),
type = value.type,
@ -68,7 +68,7 @@ internal class BalanceItemConverter(
pendingActions = value.pendingActions.toPersistentList(),
isClickable = value.isClickable(),
isPending = value.isPending,
validatorAddress = value.validatorAddress,
targetAddress = value.validatorAddress,
)
}
}
@ -108,7 +108,7 @@ internal class BalanceItemConverter(
resourceReference(R.string.staking_tap_to_unlock)
}
BalanceType.PREPARING -> {
val warmupPeriod = yield.metadata.warmupPeriod.days
val warmupPeriod = integration.warmupPeriodDays
combinedReference(
resourceReference(R.string.staking_details_warmup_period),
stringReference(" "),
@ -124,7 +124,7 @@ internal class BalanceItemConverter(
}
private fun getUnbondingDate(date: Instant?): TextReference? {
val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null
val unbondingPeriod = integration.cooldownPeriodDays ?: return null
if (date == null) {
return combinedReference(
resourceReference(R.string.staking_details_unbonding_period),

View file

@ -10,7 +10,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.utils.Provider
@ -21,7 +22,7 @@ import java.math.BigDecimal
internal class RewardsValidatorStateConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<Unit, StakingStates.RewardsValidatorsState> {
override fun convert(value: Unit): StakingStates.RewardsValidatorsState {
val stakingBalance = cryptoCurrencyStatus.value.stakingBalance
@ -42,13 +43,13 @@ internal class RewardsValidatorStateConverter(
private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) =
this.mapNotNull { balance ->
val validator = yield.validators.firstOrNull {
val target = integration.targets.firstOrNull {
it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true)
}
val cryptoValue = balance.amount
val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue)
validator?.toBalanceState(
target?.toBalanceState(
balance = balance,
cryptoCurrencyStatus = cryptoCurrencyStatus,
cryptoValue = cryptoValue,
@ -56,7 +57,7 @@ internal class RewardsValidatorStateConverter(
)
}
private fun Yield.Validator.toBalanceState(
private fun StakingTarget.toBalanceState(
balance: BalanceItem,
cryptoCurrencyStatus: CryptoCurrencyStatus,
cryptoValue: BigDecimal,
@ -80,7 +81,7 @@ internal class RewardsValidatorStateConverter(
return BalanceState(
groupId = balance.groupId,
validator = this,
target = this,
title = stringReference(this.name),
subtitle = null,
cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals),
@ -93,7 +94,7 @@ internal class RewardsValidatorStateConverter(
isClickable = true,
type = balance.type,
isPending = balance.isPending,
validatorAddress = balance.validatorAddress,
targetAddress = balance.validatorAddress,
)
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.YieldReward
@ -25,11 +25,11 @@ internal class YieldBalancesConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<Unit, InnerYieldBalanceState> {
private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) {
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
}
override fun convert(value: Unit): InnerYieldBalanceState {

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.FetchActionsUseCase
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
@ -27,7 +27,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
@DelayedWork private val coroutineScope: CoroutineScope,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
) {
fun updateAfterTransaction() {
coroutineScope.launch {
@ -105,7 +105,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fetchActionsUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
networkType = yield.token.network,
networkType = integration.token.network,
stakingActionStatus = StakingActionStatus.PROCESSING,
)
}
@ -115,7 +115,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
): StakingBalanceUpdater
}

View file

@ -12,12 +12,11 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.EstimateGasUseCase
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
@ -45,7 +44,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
) {
suspend fun getFee(
@ -58,9 +57,9 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
?: error("Illegal state")
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator?.address
?: state.balanceState?.validatorAddress
?: error("No validator address provided")
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
?: state.balanceState?.targetAddress
?: error("No target address provided")
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
@ -170,11 +169,11 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = stateController.value.actionType,
integrationId = yield.id,
integrationId = integration.integrationId.value,
amount = amount,
address = sourceAddress,
validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough,
type = action?.type,
),
@ -234,7 +233,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
): StakingFeeTransactionLoader
}
}

View file

@ -13,15 +13,14 @@ import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
import com.tangem.domain.staking.GetStakingTransactionsUseCase
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
import com.tangem.domain.staking.SubmitHashUseCase
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
@ -56,12 +55,12 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
@Assisted private val isAmountSubtractAvailable: Boolean,
) {
private val balanceUpdater: StakingBalanceUpdater
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield)
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
suspend fun constructAndSendTransactions(
onConstructSuccess: (List<StakingTransaction>) -> Unit,
@ -193,7 +192,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
val amountState = state.amountState as? AmountState.Data
?: error("No amount provided")
val validatorAddress = validatorState.chosenValidator.address
val validatorAddress = validatorState.chosenTarget.address
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
return getStakingTransactionsUseCase(
@ -201,11 +200,11 @@ internal class StakingTransactionSender @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = state.actionType,
integrationId = yield.id,
integrationId = integration.integrationId.value,
amount = amount,
address = defaultAddress,
validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough,
type = action?.type,
),
@ -306,7 +305,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
isAmountSubtractAvailable: Boolean,
): StakingTransactionSender
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
@ -81,7 +82,7 @@ internal object InitialStakingStatePreview {
fiatAmount = null,
formattedFiatAmount = stringReference("100 $"),
rawCurrencyId = null,
validator = Yield.Validator(
target = Yield.Validator(
address = "address",
status = Yield.Validator.ValidatorStatus.ACTIVE,
name = "Binance",
@ -92,13 +93,13 @@ internal object InitialStakingStatePreview {
votingPower = null,
preferred = false,
isStrategicPartner = false,
),
).toStakingTarget(),
pendingActions = persistentListOf(),
isClickable = true,
type = BalanceType.STAKED,
subtitle = null,
isPending = false,
validatorAddress = "",
targetAddress = "",
),
),
),

View file

@ -1,7 +1,11 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.features.staking.impl.presentation.state.StakingStates
import java.math.BigDecimal
@ -13,9 +17,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "Luganodes",
image = "https://assets.stakek.it/validators/luganodes.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.054823398040640445"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.1,
stakedBalance = "355544384.45009977",
@ -29,9 +33,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "InfStones",
image = "https://assets.stakek.it/validators/infstones.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.05,
stakedBalance = "12495684.05643019",
@ -45,9 +49,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "Kiln",
image = "https://assets.stakek.it/validators/kiln.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.05,
stakedBalance = "85400369.96393165",
@ -58,11 +62,13 @@ internal object ValidatorStatePreviewData {
),
)
private val targetList: List<StakingTarget> = validatorList.map { it.toStakingTarget() }
val validatorState = StakingStates.ValidatorState.Data(
availableValidators = validatorList,
chosenValidator = validatorList.first(),
availableTargets = targetList,
chosenTarget = targetList.first(),
isPrimaryButtonEnabled = true,
activeValidator = null,
activeTarget = null,
isClickable = true,
isVisibleOnConfirmation = true,
)

View file

@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
@ -40,7 +40,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun openValidators() {}
override fun onValidatorSelect(validator: Yield.Validator) {}
override fun onTargetSelect(target: StakingTarget) {}
override fun openRewardsValidators() {}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
@ -24,7 +24,7 @@ internal class SetConfirmationStateInitTransformer(
private val stakingApproval: StakingApproval,
private val stakingAllowance: BigDecimal,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yieldArgs: Yield.Args,
private val integration: StakingIntegration,
private val pendingActions: ImmutableList<PendingAction>? = null,
private val pendingAction: PendingAction? = pendingActions?.firstOrNull(),
) : Transformer<StakingUiState> {
@ -66,7 +66,7 @@ internal class SetConfirmationStateInitTransformer(
}
private fun getActionType(prevState: StakingUiState): StakingActionCommonType {
val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled
val isPartialEnterAmountDisabled = integration.isPartialAmountDisabled
val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState)
return when {
isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled)
@ -87,12 +87,12 @@ internal class SetConfirmationStateInitTransformer(
private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean {
val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId)
val isValidatorPreferred = balanceState?.validator?.preferred == true
val isTargetPreferred = balanceState?.target?.isPreferred == true
return if (isSolana && !isValidatorPreferred) {
return if (isSolana && !isTargetPreferred) {
true
} else {
yieldArgs.exit?.isPartialAmountDisabled == true
integration.exitArgs?.isPartialAmountDisabled == true
}
}
}

View file

@ -8,7 +8,7 @@ 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.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.FeeState
@ -18,7 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.utils.getRewardSchedu
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateLoadingTransformer(
private val yield: Yield,
private val integration: StakingIntegration,
private val appCurrency: AppCurrency,
private val cryptoCurrency: CryptoCurrency,
) : Transformer<StakingUiState> {
@ -48,7 +48,7 @@ internal class SetConfirmationStateLoadingTransformer(
)
}
val rewardSchedule = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule,
rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrency.network.rawId,
decapitalize = true,
)

View file

@ -19,6 +19,9 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
@ -41,7 +44,7 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
internal class SetInitialDataStateTransformer(
private val clickIntents: StakingClickIntents,
private val yield: Yield,
private val integration: StakingIntegration,
private val isAnyTokenStaked: Boolean,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val userWalletProvider: Provider<UserWallet>,
@ -55,7 +58,7 @@ internal class SetInitialDataStateTransformer(
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
}
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
@ -63,7 +66,7 @@ internal class SetInitialDataStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyProvider = appCurrencyProvider,
balancesToShowProvider = balancesToShowProvider,
yield = yield,
integration = integration,
)
}
@ -116,27 +119,27 @@ internal class SetInitialDataStateTransformer(
}
private fun createAnnualPercentageItem(): RoundedListWithDividersItemData {
val validators = yield.preferredValidators
val rateRangeInfo = getPercentageRange(validators)
val targets = integration.preferredTargets
val rateRangeInfo = getPercentageRange(targets)
return RoundedListWithDividersItemData(
id = R.string.staking_details_annual_percentage_rate,
startText = getRateStartText(rateRangeInfo.first),
endText = rateRangeInfo.second,
iconClick = {
when (rateRangeInfo.first) {
Yield.RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
Yield.RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
Yield.RewardType.UNKNOWN -> {}
RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
RewardType.UNKNOWN -> {}
}
},
isEndTextHighlighted = false,
)
}
private fun getRateStartText(rewardType: Yield.RewardType): TextReference {
private fun getRateStartText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY
}
}
@ -153,7 +156,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days ?: return null
val cooldownPeriodDays = integration.cooldownPeriodDays ?: return null
return RoundedListWithDividersItemData(
id = R.string.staking_details_unbonding_period,
startText = TextReference.Res(R.string.staking_details_unbonding_period),
@ -169,7 +172,7 @@ internal class SetInitialDataStateTransformer(
private fun createMinimumRequirementItem(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): RoundedListWithDividersItemData? {
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null
val minimumCryptoAmount = integration.enterMinimumAmount ?: return null
val blockchainId = cryptoCurrencyStatus.currency.network.rawId
if (!showMinimumRequirementInfo(blockchainId)) return null
@ -183,7 +186,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createRewardClaimingItem(): RoundedListWithDividersItemData? {
val rewardClaiming = yield.metadata.rewardClaiming
val rewardClaiming = integration.rewardClaiming
val endTextId = rewardClaimingResources[rewardClaiming] ?: return null
return RoundedListWithDividersItemData(
@ -195,7 +198,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
val warmupPeriodDays = yield.metadata.warmupPeriod.days
val warmupPeriodDays = integration.warmupPeriodDays
if (warmupPeriodDays == 0) return null
return RoundedListWithDividersItemData(
@ -212,7 +215,7 @@ internal class SetInitialDataStateTransformer(
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
val endTextReference = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule,
rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrencyStatus.currency.network.rawId,
decapitalize = false,
) ?: return null
@ -252,15 +255,19 @@ internal class SetInitialDataStateTransformer(
)
}
private fun getPercentageRange(validators: List<Yield.Validator>): Pair<Yield.RewardType, TextReference> {
if (validators.isEmpty()) {
return Yield.RewardType.APR to stringReference(DASH_SIGN)
private fun getPercentageRange(targets: List<StakingTarget>): Pair<RewardType, TextReference> {
if (targets.isEmpty()) {
return RewardType.APR to stringReference(DASH_SIGN)
}
val rewardInfos = validators
.filter { it.preferred }
val rewardInfos = targets
.filter { it.isPreferred }
.takeIf { it.isNotEmpty() }
?.mapNotNull { it.rewardInfo }
?: validators.mapNotNull { it.rewardInfo }
?: targets.mapNotNull { it.rewardInfo }
if (rewardInfos.isEmpty()) {
return RewardType.APR to stringReference(DASH_SIGN)
}
val infoWithMinRate = rewardInfos.minBy { it.rate }
val infoWithMaxRate = rewardInfos.maxBy { it.rate }

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountChangeStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val value: String,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -41,7 +41,7 @@ internal class AmountChangeStateTransformer(
amountState = AmountRequirementStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxAmount = maxEnterAmount,
yield = yield,
integration = integration,
actionType = prevState.actionType,
).transform(updatedAmountState),
)

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountMaxValueStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val actionType: StakingActionCommonType,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -38,7 +38,7 @@ internal class AmountMaxValueStateTransformer(
amountState = AmountRequirementStateTransformer(
maxAmount = maxEnterAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
yield = yield,
integration = integration,
actionType = prevState.actionType,
).transform(updatedAmountState),
)

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
@ -26,7 +27,7 @@ import java.math.RoundingMode
internal class AmountRequirementStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxAmount: EnterAmountBoundary,
private val yield: Yield,
private val integration: StakingIntegration,
private val actionType: StakingActionCommonType,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
@ -96,11 +97,11 @@ internal class AmountRequirementStateTransformer(
return when (actionType) {
is StakingActionCommonType.Enter -> {
val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
val enterRequirements = integration.enterArgs?.args?.get(Yield.Args.ArgType.AMOUNT)
enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error)
}
is StakingActionCommonType.Exit -> {
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT)
val exitRequirements = integration.exitArgs?.args?.get(Yield.Args.ArgType.AMOUNT)
exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error)
}
else -> null

View file

@ -37,7 +37,7 @@ internal class ShowApprovalBottomSheetTransformer(
val fee = feeState.fee ?: return prevState
val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty()
val validatorAddress = validatorState.chosenValidator.address
val targetAddress = validatorState.chosenTarget.address
val feeCryptoValue = fee.amount.value.format {
crypto(fee.amount.currencySymbol, fee.amount.decimals)
}
@ -57,7 +57,7 @@ internal class ShowApprovalBottomSheetTransformer(
amount = amountState.amountTextField.value,
approveType = ApproveType.UNLIMITED,
walletAddress = walletAddress,
spenderAddress = validatorAddress,
spenderAddress = targetAddress,
fee = resourceReference(
R.string.common_crypto_fiat_format,
wrappedList(feeCryptoValue, feeFiatValue),

View file

@ -15,9 +15,9 @@ import com.tangem.core.ui.extensions.stringReference
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.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.StakingErrors
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -47,12 +47,12 @@ internal class AddStakingNotificationsTransformer(
private val stakingError: StakingError?,
private val currencyCheck: CryptoCurrencyCheck,
private val isSubtractAvailable: Boolean,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
yield = yield,
integration = integration,
isSubtractAvailable = isSubtractAvailable,
)
@ -77,7 +77,7 @@ internal class AddStakingNotificationsTransformer(
isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy,
)
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum.orZero()
val minimumRequirement = integration.enterMinimumAmount.orZero()
val sendingAmount = if (isEnterAction) {
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isSubtractAvailable,

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
@ -26,7 +27,7 @@ import java.math.BigDecimal
internal class StakingInfoNotificationsFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val yield: Yield,
private val integration: StakingIntegration,
private val isSubtractAvailable: Boolean,
) {
@ -94,7 +95,7 @@ internal class StakingInfoNotificationsFactory(
resourceReference(R.string.staking_notification_withdraw_text)
}
StakingActionType.UNLOCK_LOCKED -> {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
val cooldownPeriodDays = integration.cooldownPeriodDays
if (cooldownPeriodDays != null) {
resourceReference(R.string.staking_unlocked_locked) to resourceReference(
R.string.staking_notification_unlock_text,
@ -213,7 +214,7 @@ internal class StakingInfoNotificationsFactory(
if (prevState.actionType !is StakingActionCommonType.Exit) return
val maxAmount = prevState.balanceState?.cryptoAmount ?: return
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
val exitRequirements = integration.exitArgs?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
val amountLeft = maxAmount - actionAmount
val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero()
@ -224,7 +225,7 @@ internal class StakingInfoNotificationsFactory(
}
private fun MutableList<NotificationUM>.addUnstakeInfoNotification() {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
val cooldownPeriodDays = integration.cooldownPeriodDays
val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId
if (cooldownPeriodDays != null) {
@ -248,18 +249,17 @@ internal class StakingInfoNotificationsFactory(
val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data
val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances
val validatorAddress = prevState.balanceState?.validator?.address ?: return
val targetAddress = prevState.balanceState?.target?.address ?: return
val stakesCountWithCertainValidator = stakingBalances.orEmpty()
.filter {
it.type == BalanceType.STAKED ||
it.type == BalanceType.PREPARING ||
it.type == BalanceType.UNSTAKED
val stakesCountWithCertainTarget = stakingBalances.orEmpty()
.count { state ->
(state.type == BalanceType.STAKED ||
state.type == BalanceType.PREPARING ||
state.type == BalanceType.UNSTAKED) &&
state.target?.address == targetAddress
}
.filter { it.validator?.address == validatorAddress }
.size
if (stakesCountWithCertainValidator > 1) {
if (stakesCountWithCertainTarget > 1) {
add(
StakingNotification.Info.Ordinary(
title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title),

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.transformers.validator
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -9,8 +10,8 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class ValidatorSelectChangeTransformer(
private val yield: Yield,
private val selectedValidator: Yield.Validator?,
private val integration: StakingIntegration,
private val selectedTarget: StakingTarget?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -23,27 +24,27 @@ internal class ValidatorSelectChangeTransformer(
val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo
val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED
val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake }
?: validatorState?.activeValidator
val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator }
val activeTarget = selectedTarget.takeIf { isFromInfoScreen && isRestake }
?: validatorState?.activeTarget
val filteredTargets = integration.preferredTargets.filterNot { it == activeTarget }
val selectedValidator = if (isRestake && isFromInfoScreen) {
filteredValidators.firstOrNull()
val selectedTarget = if (isRestake && isFromInfoScreen) {
filteredTargets.firstOrNull()
} else {
selectedValidator
selectedTarget
}
if (selectedValidator == null && yield.preferredValidators.isEmpty()) {
if (selectedTarget == null && integration.preferredTargets.isEmpty()) {
return prevState
}
return prevState.copy(
validatorState = StakingStates.ValidatorState.Data(
chosenValidator = selectedValidator ?: yield.preferredValidators.first(),
availableValidators = filteredValidators,
chosenTarget = selectedTarget ?: integration.preferredTargets.first(),
availableTargets = filteredTargets,
isPrimaryButtonEnabled = true,
isClickable = yield.preferredValidators.size > 1,
activeValidator = activeValidator,
isClickable = integration.preferredTargets.size > 1,
activeTarget = activeTarget,
isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked,
),
)

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.utils
import com.tangem.core.ui.extensions.*
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE
@ -52,18 +53,18 @@ internal fun getRewardScheduleText(
}
}
internal fun getRewardTypeShortText(rewardType: Yield.RewardType): TextReference {
internal fun getRewardTypeShortText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
else -> TextReference.EMPTY
}
}
internal fun getRewardTypeLongText(rewardType: Yield.RewardType): TextReference {
internal fun getRewardTypeLongText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY
}
}

View file

@ -15,7 +15,7 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -30,7 +30,7 @@ internal fun StakingClaimRewardsValidatorContent(
) {
if (state !is StakingStates.RewardsValidatorsState.Data) return
Column(
modifier = Modifier // Do not put fillMaxSize() in here
modifier = modifier // This function shouldn't itself apply fillMaxSize(); callers should avoid passing it here
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing12)
.verticalScroll(rememberScrollState()),
@ -42,9 +42,9 @@ internal fun StakingClaimRewardsValidatorContent(
caption = item.getAprTextNeutral(),
infoTitle = item.formattedFiatAmount,
infoSubtitle = item.formattedCryptoAmount,
imageUrl = item.validator?.image.orEmpty(),
imageUrl = item.target?.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() },
modifier = modifier
modifier = Modifier
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
.background(TangemTheme.colors.background.action)
.clickable(
@ -65,11 +65,11 @@ internal fun StakingClaimRewardsValidatorContent(
@Suppress("UnusedPrivateMember")
@Composable
private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
@ -77,6 +77,6 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable
private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
)

View file

@ -46,7 +46,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingDetailsScreenTestTags
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -317,11 +317,11 @@ private fun StakeButtonBlock(buttonState: NavigationButtonsState) {
@Suppress("UnusedPrivateMember")
@Composable
private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
@ -329,8 +329,8 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable
private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
)
@Composable
@ -347,7 +347,7 @@ private fun BalanceState.getImage() = when (type) {
BalanceType.UNSTAKED,
BalanceType.LOCKED,
-> null
else -> validator?.image
else -> target?.image
}
private val textGradientColors = listOf(

View file

@ -27,7 +27,8 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -54,24 +55,24 @@ internal fun StakingValidatorListContent(
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
if (state is StakingStates.ValidatorState.Data) {
val validators = state.availableValidators
val targets = state.availableTargets
items(
count = validators.size,
key = { validators[it].address },
contentType = { validators[it]::class.java },
count = targets.size,
key = { targets[it].address },
contentType = { targets[it]::class.java },
) { index ->
val item = validators[index]
val item = targets[index]
InputRowImageSelector(
subtitle = stringReference(item.name),
caption = item.getAprTextNeutral(),
imageUrl = item.image.orEmpty(),
isSelected = item == state.chosenValidator,
onSelect = { clickIntents.onValidatorSelect(item) },
isSelected = item == state.chosenTarget,
onSelect = { clickIntents.onTargetSelect(item) },
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = validators.lastIndex,
lastIndex = targets.lastIndex,
radius = TangemTheme.dimens.radius12,
addDefaultPadding =
false,
@ -106,21 +107,21 @@ internal fun StakingValidatorListContent(
*/
@Suppress("UnusedPrivateMember")
@Composable
private fun Yield.Validator.getAprTextColored() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
private fun StakingTarget.getAprTextColored() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = rewardInfo?.rate?.orZero().format { percent() },
text = rewardInfo?.rate.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
)
@Composable
private fun Yield.Validator.getAprTextNeutral() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + rewardInfo?.rate?.orZero().format { percent() }),
private fun StakingTarget.getAprTextNeutral() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + rewardInfo?.rate.orZero().format { percent() }),
)
@Composable

View file

@ -25,8 +25,9 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates.ValidatorState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.utils.getRewardTypeShortText
import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder
@ -63,20 +64,20 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic
modifier = Modifier.padding(12.dp),
) {
InputRowAsyncImage(
imageUrl = validatorState.chosenValidator.image.orEmpty(),
imageUrl = state.chosenTarget.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() },
modifier = Modifier
.size(24.dp)
.clip(TangemTheme.shapes.roundedCornersXLarge),
)
Text(
text = validatorState.chosenValidator.name,
text = state.chosenTarget.name,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerWMax()
Text(
text = validatorState.getInfoTitleNeutral().resolveReference(),
text = state.getInfoTitleNeutral().resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
)
@ -91,16 +92,16 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic
@Composable
private fun StakingStates.ValidatorState.Data.getInfoTitleColored() = combinedReference(
annotatedReference {
append(getRewardTypeShortText(chosenValidator.rewardInfo?.type ?: Yield.RewardType.UNKNOWN).resolveReference())
append(getRewardTypeShortText(chosenTarget.rewardInfo?.type ?: RewardType.UNKNOWN).resolveReference())
appendSpace()
appendColored(
text = chosenValidator.rewardInfo?.rate.orZero().format { percent() },
text = chosenTarget.rewardInfo?.rate.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
)
private fun StakingStates.ValidatorState.Data.getInfoTitleNeutral() = combinedReference(
getRewardTypeShortText(chosenValidator.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + chosenValidator.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(chosenTarget.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + chosenTarget.rewardInfo?.rate.orZero().format { percent() }),
)