Updated on 2026-08-14
This commit is contained in:
parent
fcb3722ca0
commit
df10b4fd21
17 changed files with 559 additions and 38 deletions
|
|
@ -73,7 +73,7 @@ object MockYieldDTOFactory {
|
|||
type = "type",
|
||||
rewardSchedule = YieldDTO.MetadataDTO.RewardScheduleDTO.DAY,
|
||||
cooldownPeriod = null,
|
||||
warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1),
|
||||
warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1, seconds = null),
|
||||
rewardClaiming = YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO,
|
||||
defaultValidator = null,
|
||||
minimumStake = null,
|
||||
|
|
|
|||
|
|
@ -150,6 +150,8 @@ data class YieldDTO(
|
|||
data class PeriodDTO(
|
||||
@Json(name = "days")
|
||||
val days: Int?,
|
||||
@Json(name = "seconds")
|
||||
val seconds: Int?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -326,6 +326,12 @@
|
|||
<string name="common_hide">Скрыть</string>
|
||||
<string name="common_hold_to">Удерживайте, чтобы %s</string>
|
||||
<string name="common_hour">час</string>
|
||||
<plurals name="common_hours">
|
||||
<item quantity="one">%d час</item>
|
||||
<item quantity="few">%d часa</item>
|
||||
<item quantity="many">%d часов</item>
|
||||
<item quantity="other">%d часа</item>
|
||||
</plurals>
|
||||
<plurals name="common_hours_time_ago">
|
||||
<item quantity="one">%dч назад</item>
|
||||
<item quantity="few">%dч назад</item>
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@
|
|||
<string name="common_access_denied">Access denied</string>
|
||||
<string name="common_account">Account</string>
|
||||
<string name="common_accounts">Accounts</string>
|
||||
<string name="common_action_failed">%s failed</string>
|
||||
<string name="common_activate">Activate</string>
|
||||
<string name="common_add">Add</string>
|
||||
<string name="common_add_funds">Add funds</string>
|
||||
|
|
@ -233,6 +234,8 @@
|
|||
<string name="common_apply">Apply</string>
|
||||
<string name="common_approval">Approval</string>
|
||||
<string name="common_approve">Approve</string>
|
||||
<string name="common_approved">Approved</string>
|
||||
<string name="common_approving">Approving</string>
|
||||
<string name="common_attention">Attention</string>
|
||||
<string name="common_available_networks">Available networks</string>
|
||||
<string name="common_backup">Backup</string>
|
||||
|
|
@ -315,6 +318,10 @@
|
|||
<string name="common_hide">Hide</string>
|
||||
<string name="common_hold_to">Hold to %s</string>
|
||||
<string name="common_hour">hour</string>
|
||||
<plurals name="common_hours">
|
||||
<item quantity="one">%d hour</item>
|
||||
<item quantity="other">%d hours</item>
|
||||
</plurals>
|
||||
<plurals name="common_hours_time_ago">
|
||||
<item quantity="one">%dh ago</item>
|
||||
<item quantity="other">%dh ago</item>
|
||||
|
|
@ -364,6 +371,8 @@
|
|||
<string name="common_range_with_space">%1$s — %2$s</string>
|
||||
<string name="common_read_more">Read more</string>
|
||||
<string name="common_receive">Receive</string>
|
||||
<string name="common_received">Received</string>
|
||||
<string name="common_receiving">Receiving</string>
|
||||
<string name="common_recommended">Recommended</string>
|
||||
<string name="common_reject">Reject</string>
|
||||
<string name="common_reload">Reload</string>
|
||||
|
|
@ -382,6 +391,8 @@
|
|||
<string name="common_send">Send</string>
|
||||
<string name="common_send_colon">Send:</string>
|
||||
<string name="common_send_tx_error">Failed to send transaction</string>
|
||||
<string name="common_sending">Sending</string>
|
||||
<string name="common_sent">Sent</string>
|
||||
<string name="common_server_unavailable">The server is not available, please try again later</string>
|
||||
<string name="common_share">Share</string>
|
||||
<string name="common_share_link">Share Link</string>
|
||||
|
|
@ -392,6 +403,7 @@
|
|||
<string name="common_skip">Skip</string>
|
||||
<string name="common_something_went_wrong">Something went wrong</string>
|
||||
<string name="common_stake">Stake</string>
|
||||
<string name="common_staked">Staked</string>
|
||||
<string name="common_staking">Staking</string>
|
||||
<string name="common_start">Start</string>
|
||||
<string name="common_submit">Submit</string>
|
||||
|
|
@ -399,6 +411,8 @@
|
|||
<string name="common_support">Support</string>
|
||||
<string name="common_supported_networks">Supported networks</string>
|
||||
<string name="common_swap">Swap</string>
|
||||
<string name="common_swapped">Swapped</string>
|
||||
<string name="common_swapping">Swapping</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_tap_and_hold_hint">Tap and hold</string>
|
||||
|
|
@ -416,6 +430,7 @@
|
|||
<string name="common_transaction_status">Transaction status</string>
|
||||
<string name="common_transactions">Transactions</string>
|
||||
<string name="common_transfer">Transfer</string>
|
||||
<string name="common_transferred">Transferred</string>
|
||||
<string name="common_unable_to_load">Unable to load data…</string>
|
||||
<string name="common_understand">I understand</string>
|
||||
<string name="common_understand_continue">I understand, continue</string>
|
||||
|
|
@ -425,10 +440,12 @@
|
|||
<string name="common_unstake">Unstake</string>
|
||||
<string name="common_utxo_validate_withdrawal_message_warning">Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
|
||||
<string name="common_value_copied">Value copied</string>
|
||||
<string name="common_voting">Voting</string>
|
||||
<string name="common_wallets">Wallets</string>
|
||||
<string name="common_warning">Warning</string>
|
||||
<string name="common_week">week</string>
|
||||
<string name="common_with">with</string>
|
||||
<string name="common_withdrawing">Withdrawing</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="common_yield_mode">Yield Mode</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
|
|
@ -590,6 +607,7 @@
|
|||
<string name="express_provider_best_rate">Best rate</string>
|
||||
<string name="express_provider_fca_warning_list">FCA Warning List</string>
|
||||
<string name="express_provider_fixed_rate_is_unavailable">Fixed rate is unavailable</string>
|
||||
<string name="express_provider_for_swap">Provider for swap</string>
|
||||
<string name="express_provider_great_rate">Competitive rate</string>
|
||||
<string name="express_provider_in_fca_warning_list">Provider in FCA warning list</string>
|
||||
<string name="express_provider_max_amount">Available up to %s</string>
|
||||
|
|
@ -1334,6 +1352,8 @@
|
|||
<string name="send_notification_invalid_reserve_amount_text">Target account is not created. Please change the amount to send.</string>
|
||||
<string name="send_notification_invalid_reserve_amount_title">The amount to send must be at least %s</string>
|
||||
<string name="send_notification_leave_button">Leave %s</string>
|
||||
<string name="send_notification_no_trustline_text">A trustline for %s is required first.</string>
|
||||
<string name="send_notification_no_trustline_title">Can\'t receive token</string>
|
||||
<string name="send_notification_reduce_by">Reduce by %s</string>
|
||||
<string name="send_notification_reduce_to">Reduce to %s</string>
|
||||
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
|
||||
|
|
@ -1820,12 +1840,21 @@
|
|||
<string name="tokens_list_unavailable_to_sell_header">Unavailable to sell</string>
|
||||
<string name="tokens_list_unavailable_to_swap_header">Unavailable for swap from %s</string>
|
||||
<string name="tokens_list_unavailable_to_swap_source_header">Unavailable for swap</string>
|
||||
<string name="transaction_history_claiming_reward">Claiming reward</string>
|
||||
<string name="transaction_history_contract_address">contract: %s</string>
|
||||
<string name="transaction_history_disabling_yield_mode">Disabling Yield mode</string>
|
||||
<string name="transaction_history_earned_from_stake">Earned from stake</string>
|
||||
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
|
||||
<string name="transaction_history_error_failed_to_load">Failed to load transaction history.\nClick on reload button to update the information.</string>
|
||||
<string name="transaction_history_from_inline_address">from: %%image%% %s</string>
|
||||
<string name="transaction_history_multiple_addresses">Multiple addresses</string>
|
||||
<string name="transaction_history_not_supported_description">Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer.</string>
|
||||
<string name="transaction_history_operation">Operation</string>
|
||||
<string name="transaction_history_pending">Pending</string>
|
||||
<string name="transaction_history_rewards_restaked">Rewards restaked</string>
|
||||
<string name="transaction_history_rewards_restaking">Rewards restaking</string>
|
||||
<string name="transaction_history_staking_reward">Staking reward</string>
|
||||
<string name="transaction_history_to_inline_address">to: %%image%% %s</string>
|
||||
<string name="transaction_history_transaction_for_address">for: %s</string>
|
||||
<string name="transaction_history_transaction_from_address">from: %s</string>
|
||||
<string name="transaction_history_transaction_to_address">to: %s</string>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import org.joda.time.DateTimeZone
|
|||
import org.joda.time.LocalDate
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
|
||||
const val SECONDS_IN_HOUR = 3600
|
||||
|
||||
/**
|
||||
* If [this] timestamp is today or yesterday, returns relative date,
|
||||
* otherwise returns formatting date.
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period {
|
||||
return Yield.Metadata.Period(
|
||||
days = periodDTO.days.asMandatory("days"),
|
||||
seconds = periodDTO.seconds,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ data class Yield(
|
|||
@Serializable
|
||||
data class Period(
|
||||
val days: Int,
|
||||
val seconds: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.domain.staking.model
|
|||
|
||||
sealed class CooldownPeriod {
|
||||
|
||||
data class Fixed(val days: Int) : CooldownPeriod()
|
||||
data class Fixed(val period: Period) : CooldownPeriod()
|
||||
|
||||
data class Range(val minDays: Int, val maxDays: Int) : CooldownPeriod()
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ class P2PEthPoolIntegration(
|
|||
|
||||
// Metadata
|
||||
|
||||
override val warmupPeriodDays: Int = 0
|
||||
override val warmupPeriod: Period = Period.Days(0)
|
||||
|
||||
override val cooldownPeriod: CooldownPeriod = CooldownPeriod.Range(
|
||||
minDays = MIN_COOLDOWN_DAYS,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
sealed class Period {
|
||||
|
||||
abstract val value: Int
|
||||
|
||||
data class Days(
|
||||
override val value: Int,
|
||||
) : Period()
|
||||
|
||||
data class Seconds(
|
||||
override val value: Int,
|
||||
) : Period()
|
||||
}
|
||||
|
|
@ -49,10 +49,22 @@ class StakeKitIntegration(
|
|||
|
||||
// Metadata
|
||||
|
||||
override val warmupPeriodDays: Int = yield.metadata.warmupPeriod.days
|
||||
override val warmupPeriod: Period = yield.metadata.warmupPeriod.let { period ->
|
||||
period.seconds?.let {
|
||||
Period.Seconds(it)
|
||||
} ?: period.days.let {
|
||||
Period.Days(it)
|
||||
}
|
||||
}
|
||||
|
||||
override val cooldownPeriod: CooldownPeriod? = yield.metadata.cooldownPeriod?.days?.let {
|
||||
CooldownPeriod.Fixed(it)
|
||||
override val cooldownPeriod: CooldownPeriod? = yield.metadata.cooldownPeriod?.let { period ->
|
||||
CooldownPeriod.Fixed(
|
||||
period.seconds?.let {
|
||||
Period.Seconds(it)
|
||||
} ?: period.days.let {
|
||||
Period.Days(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override val rewardSchedule: RewardSchedule = yield.metadata.rewardSchedule.toRewardSchedule()
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ sealed interface StakingIntegration {
|
|||
|
||||
// Metadata
|
||||
|
||||
val warmupPeriodDays: Int
|
||||
val warmupPeriod: Period
|
||||
|
||||
val cooldownPeriod: CooldownPeriod?
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.stakekit.AddressArgument
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Tests for [StakeKitIntegration] — specifically the Period/CooldownPeriod mapping from [Yield.Metadata].
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class StakeKitIntegrationTest {
|
||||
|
||||
// region helpers
|
||||
|
||||
private val dummyToken = YieldToken(
|
||||
name = "Test Token",
|
||||
network = NetworkType.SOLANA,
|
||||
symbol = "SOL",
|
||||
decimals = 9,
|
||||
address = null,
|
||||
coinGeckoId = null,
|
||||
logoURI = null,
|
||||
isPoints = false,
|
||||
)
|
||||
|
||||
private val dummyEnter = Yield.Args.Enter(
|
||||
addresses = Yield.Args.Enter.Addresses(
|
||||
address = AddressArgument(required = false),
|
||||
),
|
||||
args = emptyMap(),
|
||||
)
|
||||
|
||||
private val dummyArgs = Yield.Args(enter = dummyEnter, exit = null)
|
||||
|
||||
private val dummyStatus = Yield.Status(enter = true, exit = null)
|
||||
|
||||
private val dummyEnabled = Yield.Metadata.Enabled(enabled = true)
|
||||
|
||||
private fun buildYield(
|
||||
warmupPeriod: Yield.Metadata.Period,
|
||||
cooldownPeriod: Yield.Metadata.Period?,
|
||||
): Yield {
|
||||
return Yield(
|
||||
id = "test-integration",
|
||||
token = dummyToken,
|
||||
tokens = emptyList(),
|
||||
args = dummyArgs,
|
||||
status = dummyStatus,
|
||||
apy = BigDecimal("5.0"),
|
||||
rewardRate = 5.0,
|
||||
rewardType = com.tangem.domain.staking.model.common.RewardType.APY,
|
||||
metadata = Yield.Metadata(
|
||||
name = "Test Staking",
|
||||
logoUri = "https://example.com/logo.png",
|
||||
description = "Test staking integration",
|
||||
documentation = null,
|
||||
gasFeeToken = dummyToken,
|
||||
token = dummyToken,
|
||||
tokens = emptyList(),
|
||||
type = "liquid",
|
||||
rewardSchedule = Yield.Metadata.RewardSchedule.DAY,
|
||||
cooldownPeriod = cooldownPeriod,
|
||||
warmupPeriod = warmupPeriod,
|
||||
rewardClaiming = Yield.Metadata.RewardClaiming.AUTO,
|
||||
defaultValidator = null,
|
||||
minimumStake = null,
|
||||
supportsMultipleValidators = false,
|
||||
revshare = dummyEnabled,
|
||||
fee = dummyEnabled,
|
||||
),
|
||||
validators = emptyList(),
|
||||
isAvailable = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildIntegration(
|
||||
warmupPeriod: Yield.Metadata.Period,
|
||||
cooldownPeriod: Yield.Metadata.Period?,
|
||||
): StakeKitIntegration {
|
||||
return StakeKitIntegration(
|
||||
integrationId = StakingIntegrationID.StakeKit.Coin.Solana,
|
||||
yield = buildYield(warmupPeriod = warmupPeriod, cooldownPeriod = cooldownPeriod),
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class `warmupPeriod mapping` {
|
||||
|
||||
@Test
|
||||
fun `should produce Period Seconds when seconds is non-null`() {
|
||||
// given
|
||||
val warmup = Yield.Metadata.Period(days = 3, seconds = 7200)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
|
||||
|
||||
// then
|
||||
assertThat(integration.warmupPeriod).isEqualTo(Period.Seconds(7200))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should produce Period Days when seconds is null`() {
|
||||
// given
|
||||
val warmup = Yield.Metadata.Period(days = 5, seconds = null)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
|
||||
|
||||
// then
|
||||
assertThat(integration.warmupPeriod).isEqualTo(Period.Days(5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should prefer seconds over days when both are present`() {
|
||||
// given — days is non-zero but seconds takes priority
|
||||
val warmup = Yield.Metadata.Period(days = 10, seconds = 3600)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
|
||||
|
||||
// then
|
||||
assertThat(integration.warmupPeriod).isInstanceOf(Period.Seconds::class.java)
|
||||
assertThat((integration.warmupPeriod as Period.Seconds).value).isEqualTo(3600)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should produce Period Days with zero value when days is zero and seconds is null`() {
|
||||
// given
|
||||
val warmup = Yield.Metadata.Period(days = 0, seconds = null)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
|
||||
|
||||
// then
|
||||
assertThat(integration.warmupPeriod).isEqualTo(Period.Days(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class `cooldownPeriod mapping` {
|
||||
|
||||
@Test
|
||||
fun `should be null when yield cooldownPeriod is null`() {
|
||||
// given
|
||||
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
|
||||
|
||||
// then
|
||||
assertThat(integration.cooldownPeriod).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should produce Fixed Period Seconds when cooldown seconds is non-null`() {
|
||||
// given
|
||||
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
|
||||
val cooldown = Yield.Metadata.Period(days = 2, seconds = 86400)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown)
|
||||
|
||||
// then
|
||||
assertThat(integration.cooldownPeriod).isEqualTo(CooldownPeriod.Fixed(Period.Seconds(86400)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should produce Fixed Period Days when cooldown seconds is null`() {
|
||||
// given
|
||||
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
|
||||
val cooldown = Yield.Metadata.Period(days = 3, seconds = null)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown)
|
||||
|
||||
// then
|
||||
assertThat(integration.cooldownPeriod).isEqualTo(CooldownPeriod.Fixed(Period.Days(3)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should prefer seconds over days in cooldown when both are present`() {
|
||||
// given
|
||||
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
|
||||
val cooldown = Yield.Metadata.Period(days = 7, seconds = 604800)
|
||||
|
||||
// when
|
||||
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown)
|
||||
|
||||
// then
|
||||
val period = integration.cooldownPeriod
|
||||
assertThat(period).isInstanceOf(CooldownPeriod.Fixed::class.java)
|
||||
assertThat((period as CooldownPeriod.Fixed).period).isInstanceOf(Period.Seconds::class.java)
|
||||
assertThat((period.period as Period.Seconds).value).isEqualTo(604800)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,16 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.converters
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
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.extensions.*
|
||||
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.core.ui.utils.SECONDS_IN_HOUR
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.BalanceType
|
||||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingBalanceEntry
|
||||
import com.tangem.domain.models.staking.StakingEntryActions
|
||||
import com.tangem.domain.models.staking.StakingEntryType
|
||||
import com.tangem.domain.models.staking.*
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.Period
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.features.staking.impl.R
|
||||
|
|
@ -125,12 +118,26 @@ internal class StakingBalanceEntryConverter(
|
|||
}
|
||||
}
|
||||
StakingEntryType.PREPARING -> {
|
||||
val warmupPeriod = integration.warmupPeriodDays
|
||||
val warmupPeriod = integration.warmupPeriod
|
||||
TextReference.Combined(
|
||||
wrappedList(
|
||||
resourceReference(R.string.staking_details_warmup_period),
|
||||
stringReference(" "),
|
||||
pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)),
|
||||
when (warmupPeriod) {
|
||||
is Period.Days -> pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = warmupPeriod.value,
|
||||
formatArgs = wrappedList(warmupPeriod.value),
|
||||
)
|
||||
is Period.Seconds -> {
|
||||
val hours = warmupPeriod.value / SECONDS_IN_HOUR
|
||||
pluralReference(
|
||||
id = R.plurals.common_hours,
|
||||
count = hours,
|
||||
formatArgs = wrappedList(hours),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,20 @@ import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
|
|||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.SECONDS_IN_HOUR
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.StakingBalanceEntry
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.model.Period
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.common.RewardClaiming
|
||||
|
|
@ -199,17 +201,27 @@ internal class SetInitialDataStateTransformer(
|
|||
}
|
||||
|
||||
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
|
||||
val warmupPeriodDays = integration.warmupPeriodDays
|
||||
if (warmupPeriodDays == 0) return null
|
||||
val warmupPeriod = integration.warmupPeriod
|
||||
if (warmupPeriod.value == 0) return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_warmup_period,
|
||||
startText = TextReference.Res(R.string.staking_details_warmup_period),
|
||||
endText = pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = warmupPeriodDays,
|
||||
formatArgs = wrappedList(warmupPeriodDays),
|
||||
),
|
||||
endText = when (warmupPeriod) {
|
||||
is Period.Days -> pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = warmupPeriod.value,
|
||||
formatArgs = wrappedList(warmupPeriod.value),
|
||||
)
|
||||
is Period.Seconds -> {
|
||||
val hours = warmupPeriod.value / SECONDS_IN_HOUR
|
||||
pluralReference(
|
||||
id = R.plurals.common_hours,
|
||||
count = hours,
|
||||
formatArgs = wrappedList(hours),
|
||||
)
|
||||
}
|
||||
},
|
||||
iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.utils.SECONDS_IN_HOUR
|
||||
import com.tangem.domain.staking.model.CooldownPeriod
|
||||
import com.tangem.domain.staking.model.Period
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
|
||||
|
||||
internal fun CooldownPeriod.toTextReference(): TextReference {
|
||||
return when (this) {
|
||||
is CooldownPeriod.Fixed -> pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = days,
|
||||
formatArgs = wrappedList(days),
|
||||
)
|
||||
is CooldownPeriod.Fixed -> when (period) {
|
||||
is Period.Days -> pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = period.value,
|
||||
formatArgs = wrappedList(period.value),
|
||||
)
|
||||
is Period.Seconds -> {
|
||||
val hours = period.value / SECONDS_IN_HOUR
|
||||
pluralReference(
|
||||
id = R.plurals.common_hours,
|
||||
count = hours,
|
||||
formatArgs = wrappedList(hours),
|
||||
)
|
||||
}
|
||||
}
|
||||
is CooldownPeriod.Range -> combinedReference(
|
||||
stringReference("$minDays$MINUS$maxDays$NON_BREAKING_SPACE"),
|
||||
pluralReference(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.SECONDS_IN_HOUR
|
||||
import com.tangem.domain.staking.model.CooldownPeriod
|
||||
import com.tangem.domain.staking.model.Period
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
* Tests for [CooldownPeriod.toTextReference] extension function.
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class CooldownPeriodUtilsTest {
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class `Fixed with Days` {
|
||||
|
||||
@Test
|
||||
fun `should return plural days reference for Fixed Period Days`() {
|
||||
// given
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Days(7))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
val expected = pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = 7,
|
||||
formatArgs = wrappedList(7),
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return plural days with zero for Fixed Period Days zero`() {
|
||||
// given
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Days(0))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
val expected = pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = 0,
|
||||
formatArgs = wrappedList(0),
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should pass day count as both count and format arg`() {
|
||||
// given
|
||||
val days = 14
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Days(days))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
|
||||
val plural = result as TextReference.PluralRes
|
||||
assertThat(plural.count).isEqualTo(days)
|
||||
assertThat(plural.formatArgs.first()).isEqualTo(days)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class `Fixed with Seconds` {
|
||||
|
||||
@Test
|
||||
fun `should return plural hours reference for Fixed Period Seconds`() {
|
||||
// given — 2 hours worth of seconds
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Seconds(2 * SECONDS_IN_HOUR))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
val expectedHours = 2
|
||||
val expected = pluralReference(
|
||||
id = R.plurals.common_hours,
|
||||
count = expectedHours,
|
||||
formatArgs = wrappedList(expectedHours),
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should convert exactly one hour worth of seconds to 1 hour`() {
|
||||
// given
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Seconds(SECONDS_IN_HOUR))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
|
||||
val plural = result as TextReference.PluralRes
|
||||
assertThat(plural.count).isEqualTo(1)
|
||||
assertThat(plural.formatArgs.first()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should floor to 1 hour when seconds are not divisible evenly`() {
|
||||
// given — 5000 seconds = 1.388... hours → integer division → 1
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Seconds(5000))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
|
||||
val plural = result as TextReference.PluralRes
|
||||
assertThat(plural.count).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return 0 hours for zero seconds`() {
|
||||
// given
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Seconds(0))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
|
||||
val plural = result as TextReference.PluralRes
|
||||
assertThat(plural.count).isEqualTo(0)
|
||||
assertThat(plural.formatArgs.first()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should use R plurals common_hours resource id`() {
|
||||
// given
|
||||
val cooldown = CooldownPeriod.Fixed(Period.Seconds(SECONDS_IN_HOUR))
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
|
||||
assertThat((result as TextReference.PluralRes).id).isEqualTo(R.plurals.common_hours)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Range {
|
||||
|
||||
@Test
|
||||
fun `should return combined reference for Range`() {
|
||||
// given
|
||||
val minDays = 2
|
||||
val maxDays = 5
|
||||
val cooldown = CooldownPeriod.Range(minDays = minDays, maxDays = maxDays)
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
val expected = combinedReference(
|
||||
stringReference("$minDays$MINUS$maxDays$NON_BREAKING_SPACE"),
|
||||
pluralReference(
|
||||
id = R.plurals.common_days_no_param,
|
||||
count = maxDays,
|
||||
),
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should use maxDays as count for plural in Range`() {
|
||||
// given
|
||||
val maxDays = 10
|
||||
val cooldown = CooldownPeriod.Range(minDays = 3, maxDays = maxDays)
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
assertThat(result).isInstanceOf(TextReference.Combined::class.java)
|
||||
val combined = result as TextReference.Combined
|
||||
val pluralPart = combined.refs[1]
|
||||
assertThat(pluralPart).isInstanceOf(TextReference.PluralRes::class.java)
|
||||
assertThat((pluralPart as TextReference.PluralRes).count).isEqualTo(maxDays)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should include minDays and maxDays with minus and non-breaking-space in string part`() {
|
||||
// given
|
||||
val minDays = 1
|
||||
val maxDays = 7
|
||||
val cooldown = CooldownPeriod.Range(minDays = minDays, maxDays = maxDays)
|
||||
|
||||
// when
|
||||
val result = cooldown.toTextReference()
|
||||
|
||||
// then
|
||||
assertThat(result).isInstanceOf(TextReference.Combined::class.java)
|
||||
val combined = result as TextReference.Combined
|
||||
val stringPart = combined.refs[0]
|
||||
assertThat(stringPart).isInstanceOf(TextReference.Str::class.java)
|
||||
assertThat((stringPart as TextReference.Str).value)
|
||||
.isEqualTo("$minDays$MINUS$maxDays$NON_BREAKING_SPACE")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue