diff --git a/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt b/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt index 2da8dc8764..ce212da24f 100644 --- a/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt +++ b/android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt @@ -77,7 +77,7 @@ class DelegationViewModel @Inject constructor( .getValidatorUrl(getCurrentBlockExplorer.getCurrentBlockExplorer(chain), delegation.validator.id) listOfNotNull( DelegationProperty.Name(delegation.validator.name, validatorUrl), - DelegationProperty.Apr(delegation.validator), + delegation.validator.takeIf { it.apr != 0.0 }?.let { DelegationProperty.Apr(it) }, DelegationProperty.TransactionStatus(delegation.base.state, delegation.validator.isActive), delegation.base.state .takeIf { diff --git a/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/StakeScene.kt b/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/StakeScene.kt index fedfa82dab..74f3210634 100644 --- a/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/StakeScene.kt +++ b/android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/StakeScene.kt @@ -41,7 +41,6 @@ import com.gemwallet.android.ui.components.list_head.CenteredListHead import com.gemwallet.android.ui.components.list_head.HeaderIcon import com.gemwallet.android.ui.components.list_item.DelegationItem import com.gemwallet.android.ui.components.list_item.SubheaderItem -import com.gemwallet.android.ui.components.list_item.availableIn import com.gemwallet.android.ui.components.list_item.energyItem import com.gemwallet.android.ui.components.list_item.property.PropertyItem import com.gemwallet.android.ui.components.list_item.property.itemsPositioned @@ -129,7 +128,6 @@ fun StakeScene( DelegationItem( assetInfo = assetInfo, delegation = item, - completedAt = availableIn(item), listPosition = ListPosition.getPosition(index, delegations.size), onClick = { onDelegation(item) } ) @@ -150,9 +148,9 @@ private fun LazyListScope.stakeInfoSection(assetInfo: AssetInfo) { val minAmountValue = Config().getStakeConfig(assetInfo.asset.chain.string).minAmount.toLong() val iconUrl = assetInfo.id().getIconUrl() val rows = listOfNotNull( - minAmountValue.takeIf { it > 0 }?.let { StakeInfoRow.MinAmount(it, assetInfo.asset.chain) }, StakeInfoRow.Apr(assetInfo.stakeApr ?: 0.0, iconUrl), assetInfo.lockTime?.let { StakeInfoRow.LockTime(it, iconUrl) }, + minAmountValue.takeIf { it > 0 }?.let { StakeInfoRow.MinAmount(it, assetInfo.asset.chain) }, ) itemsPositioned(rows) { position, row -> when (row) { diff --git a/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/DelegationItem.kt b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/DelegationItem.kt index 9ffada5778..433deb0979 100644 --- a/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/DelegationItem.kt +++ b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/DelegationItem.kt @@ -1,7 +1,6 @@ package com.gemwallet.android.ui.components.list_item import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -29,7 +28,6 @@ import com.wallet.core.primitives.DelegationState.Pending fun DelegationItem( assetInfo: AssetInfo, delegation: Delegation, - completedAt: String, listPosition: ListPosition, onClick: () -> Unit ) { @@ -46,21 +44,7 @@ fun DelegationItem( ListItemTitleText(text = delegation.validator.name) }, subtitle = { - val stateColor = delegation.base.state.color() - val stateText = delegation.stateText() - Column { - ListItemSupportText(stateText, color = stateColor) - when (delegation.base.state) { - Pending, - Activating, - Deactivating -> completedAt.takeIf { it.isNotEmpty() && it != "0" }?.let { - ListItemSupportText(it) - } - Active, - Inactive, - AwaitingWithdrawal -> Unit - } - } + ListItemSupportText(delegation.stateText(), color = delegation.base.state.color()) }, trailing = { val balance = DelegationBalanceInfoUIModel( diff --git a/core/crates/gem_hypercore/src/provider/staking.rs b/core/crates/gem_hypercore/src/provider/staking.rs index 69ef9de08b..1a48ba075c 100644 --- a/core/crates/gem_hypercore/src/provider/staking.rs +++ b/core/crates/gem_hypercore/src/provider/staking.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use chain_traits::ChainStaking; +use chrono::Utc; use futures::try_join; use std::error::Error; @@ -22,7 +23,7 @@ impl ChainStaking for HyperCoreClient { } async fn get_staking_delegations(&self, address: String) -> Result, Box> { - let (delegations, stake_balance) = try_join!(self.get_staking_delegations(&address), self.get_stake_balance(&address))?; - Ok(staking_mapper::map_staking_delegations(delegations, stake_balance, self.chain)) + let (delegations, history) = try_join!(self.get_staking_delegations(&address), self.get_delegator_history(&address))?; + Ok(staking_mapper::map_staking_delegations(delegations, history, Utc::now(), self.chain)) } } diff --git a/core/crates/gem_hypercore/src/provider/staking_mapper.rs b/core/crates/gem_hypercore/src/provider/staking_mapper.rs index 99489ffaee..f93ebfe496 100644 --- a/core/crates/gem_hypercore/src/provider/staking_mapper.rs +++ b/core/crates/gem_hypercore/src/provider/staking_mapper.rs @@ -1,4 +1,7 @@ -use crate::models::balance::{DelegationBalance, StakeBalance, Validator}; +use crate::models::balance::{DelegationBalance, Validator}; +use crate::models::user::DelegatorHistoryUpdate; +use crate::provider::transaction_state_mapper::DELEGATOR_WITHDRAWAL_INITIATED; +use chrono::{DateTime, Duration, Utc}; use num_bigint::BigUint; use number_formatter::BigNumberFormatter; use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator}; @@ -15,7 +18,7 @@ pub fn map_staking_validators(validators: Vec, chain: Chain, apy: Opt result } -pub fn map_staking_delegations(delegations: Vec, stake_balance: StakeBalance, chain: Chain) -> Vec { +pub fn map_staking_delegations(delegations: Vec, history: Vec, now: DateTime, chain: Chain) -> Vec { let native_decimals = Asset::from_chain(chain).decimals as u32; let mut result: Vec = delegations .into_iter() @@ -31,34 +34,57 @@ pub fn map_staking_delegations(delegations: Vec, stake_balanc }) .collect(); - let pending = BigNumberFormatter::value_from_amount_biguint(&stake_balance.total_pending_withdrawal, native_decimals).unwrap_or_default(); - if pending > BigUint::from(0u32) { - result.push(DelegationBase { - asset_id: chain.as_asset_id(), - state: DelegationState::Pending, - balance: pending, - shares: BigUint::from(0u32), - rewards: BigUint::from(0u32), - completion_date: None, - delegation_id: DelegationValidator::SYSTEM_ID.to_string(), - validator_id: DelegationValidator::SYSTEM_ID.to_string(), - }); - } - + result.extend(map_pending_withdrawals(history, now, chain, native_decimals)); result } +fn map_pending_withdrawals(history: Vec, now: DateTime, chain: Chain, native_decimals: u32) -> Vec { + let lock = Duration::seconds(chain.config().stake.as_ref().map(|stake| stake.lock_time).unwrap_or_default() as i64); + + history + .into_iter() + .filter_map(|entry| { + let withdrawal = entry.delta.withdrawal?; + if withdrawal.phase != DELEGATOR_WITHDRAWAL_INITIATED { + return None; + } + let completion_date = DateTime::from_timestamp_millis(entry.time as i64)? + lock; + if completion_date <= now { + return None; + } + Some(DelegationBase { + asset_id: chain.as_asset_id(), + state: DelegationState::Pending, + balance: BigNumberFormatter::value_from_amount_biguint(&withdrawal.amount, native_decimals).unwrap_or_default(), + shares: BigUint::from(0u32), + rewards: BigUint::from(0u32), + completion_date: Some(completion_date), + delegation_id: format!("unstaking_{}", entry.time), + validator_id: DelegationValidator::SYSTEM_ID.to_string(), + }) + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; use crate::models::balance::ValidatorStats; + use crate::models::user::{DelegatorHistoryDelta, DelegatorWithdrawalDelta}; use primitives::{Chain, DelegationState}; - fn stake_balance(total_pending_withdrawal: &str) -> StakeBalance { - StakeBalance { - delegated: "0".to_string(), - undelegated: "0".to_string(), - total_pending_withdrawal: total_pending_withdrawal.to_string(), + fn withdrawal_entry(time: u64, amount: &str, phase: &str) -> DelegatorHistoryUpdate { + DelegatorHistoryUpdate { + time, + hash: "0x0".to_string(), + delta: DelegatorHistoryDelta { + c_deposit: None, + delegate: None, + withdrawal: Some(DelegatorWithdrawalDelta { + amount: amount.to_string(), + phase: phase.to_string(), + }), + }, } } @@ -107,7 +133,7 @@ mod tests { fn test_map_staking_delegations() { let delegations: Vec = serde_json::from_str(include_str!("../../testdata/staking_delegations.json")).unwrap(); - let result = map_staking_delegations(delegations, stake_balance("0"), Chain::HyperCore); + let result = map_staking_delegations(delegations, vec![], Utc::now(), Chain::HyperCore); assert_eq!(result.len(), 2); @@ -127,21 +153,33 @@ mod tests { } #[test] - fn test_map_staking_delegations_pending_withdrawal() { - let result = map_staking_delegations(vec![], stake_balance("0.015"), Chain::HyperCore); + fn test_map_staking_delegations_pending_withdrawals() { + let now = DateTime::from_timestamp(1_780_000_000, 0).unwrap(); + let at = |days: i64| (now - Duration::days(days)).timestamp_millis() as u64; + let history = vec![ + withdrawal_entry(at(1), "1.5", "initiated"), + withdrawal_entry(at(8), "2.0", "initiated"), + withdrawal_entry(at(1), "3.0", "finalized"), + DelegatorHistoryUpdate { + time: at(0), + hash: "0x0".to_string(), + delta: DelegatorHistoryDelta { + c_deposit: None, + delegate: None, + withdrawal: None, + }, + }, + ]; + + let result = map_staking_delegations(vec![], history, now, Chain::HyperCore); assert_eq!(result.len(), 1); let pending = &result[0]; assert_eq!(pending.state, DelegationState::Pending); assert_eq!(pending.validator_id, DelegationValidator::SYSTEM_ID); - assert_eq!(pending.balance.to_string(), "1500000"); - assert!(pending.completion_date.is_none()); - } - - #[test] - fn test_map_staking_delegations_no_pending_withdrawal() { - let result = map_staking_delegations(vec![], stake_balance("0"), Chain::HyperCore); - - assert!(result.is_empty()); + assert_eq!(pending.balance.to_string(), "150000000"); + assert_eq!(pending.delegation_id, format!("unstaking_{}", at(1))); + assert_eq!(pending.completion_date, Some(now + Duration::days(6))); + assert!(pending.completion_date.unwrap() > now); } } diff --git a/core/crates/gem_hypercore/src/provider/transaction_state_mapper.rs b/core/crates/gem_hypercore/src/provider/transaction_state_mapper.rs index c1847e5ddd..e8f128abec 100644 --- a/core/crates/gem_hypercore/src/provider/transaction_state_mapper.rs +++ b/core/crates/gem_hypercore/src/provider/transaction_state_mapper.rs @@ -13,7 +13,7 @@ use crate::perpetual_formatter::usdc_value; pub const ACTION_HISTORY_QUERY_LOOKBACK_MS: u64 = 5_000; const ACTION_HISTORY_MATCH_WINDOW_MS: u64 = 5 * 60 * 1_000; -const DELEGATOR_WITHDRAWAL_INITIATED: &str = "initiated"; +pub(crate) const DELEGATOR_WITHDRAWAL_INITIATED: &str = "initiated"; fn perpetual_fill_type_and_direction(dir: &FillDirection) -> Option<(TransactionType, PerpetualDirection)> { match dir { diff --git a/ios/Packages/Store/Sources/Requests/DelegationsRequest.swift b/ios/Packages/Store/Sources/Requests/DelegationsRequest.swift index 62ddade2da..762a5e14dd 100644 --- a/ios/Packages/Store/Sources/Requests/DelegationsRequest.swift +++ b/ios/Packages/Store/Sources/Requests/DelegationsRequest.swift @@ -1,5 +1,6 @@ // Copyright (c). Gem Wallet. All rights reserved. +internal import BigInt import Foundation import GRDB import Primitives @@ -23,9 +24,9 @@ public struct DelegationsRequest: DatabaseQueryable { .filter(StakeDelegationRecord.Columns.assetId == assetId.identifier) .joining(required: StakeDelegationRecord.validator .filter(StakeValidatorRecord.Columns.providerType == providerType.rawValue)) - .order(StakeDelegationRecord.Columns.balance.desc) .asRequest(of: StakeDelegationInfo.self) .fetchAll(db) .compactMap { $0.mapToDelegation() } + .sorted { $0.base.balanceValue > $1.base.balanceValue } } }