Skip to content

Commit cce7529

Browse files
authored
Show unlock date for pending unstaking (#421)
* Add unlock date for pending Hyperliquid unstaking Derive pending unstaking rows from delegatorHistory so each withdrawal carries its own unlock date, replacing the single aggregate row that had no date. Align iOS/Android staking UI: hide zero APR, sort delegations by numeric balance, reorder stake info card, and move the list countdown to the detail screen only. * Reuse existing delegator history instead of duplicate request Drop the duplicate get_staking_history method and parallel history models; map pending unstaking from the existing get_delegator_history / DelegatorHistoryUpdate, sharing the DELEGATOR_WITHDRAWAL_INITIATED phase constant.
1 parent db1e9e9 commit cce7529

7 files changed

Lines changed: 80 additions & 58 deletions

File tree

android/features/earn/delegation/viewmodels/src/main/kotlin/com/gemwallet/android/features/earn/delegation/viewmodels/DelegationViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class DelegationViewModel @Inject constructor(
7777
.getValidatorUrl(getCurrentBlockExplorer.getCurrentBlockExplorer(chain), delegation.validator.id)
7878
listOfNotNull(
7979
DelegationProperty.Name(delegation.validator.name, validatorUrl),
80-
DelegationProperty.Apr(delegation.validator),
80+
delegation.validator.takeIf { it.apr != 0.0 }?.let { DelegationProperty.Apr(it) },
8181
DelegationProperty.TransactionStatus(delegation.base.state, delegation.validator.isActive),
8282
delegation.base.state
8383
.takeIf {

android/features/earn/stake/presents/src/main/kotlin/com/gemwallet/android/features/stake/presents/StakeScene.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import com.gemwallet.android.ui.components.list_head.CenteredListHead
4141
import com.gemwallet.android.ui.components.list_head.HeaderIcon
4242
import com.gemwallet.android.ui.components.list_item.DelegationItem
4343
import com.gemwallet.android.ui.components.list_item.SubheaderItem
44-
import com.gemwallet.android.ui.components.list_item.availableIn
4544
import com.gemwallet.android.ui.components.list_item.energyItem
4645
import com.gemwallet.android.ui.components.list_item.property.PropertyItem
4746
import com.gemwallet.android.ui.components.list_item.property.itemsPositioned
@@ -129,7 +128,6 @@ fun StakeScene(
129128
DelegationItem(
130129
assetInfo = assetInfo,
131130
delegation = item,
132-
completedAt = availableIn(item),
133131
listPosition = ListPosition.getPosition(index, delegations.size),
134132
onClick = { onDelegation(item) }
135133
)
@@ -150,9 +148,9 @@ private fun LazyListScope.stakeInfoSection(assetInfo: AssetInfo) {
150148
val minAmountValue = Config().getStakeConfig(assetInfo.asset.chain.string).minAmount.toLong()
151149
val iconUrl = assetInfo.id().getIconUrl()
152150
val rows = listOfNotNull(
153-
minAmountValue.takeIf { it > 0 }?.let { StakeInfoRow.MinAmount(it, assetInfo.asset.chain) },
154151
StakeInfoRow.Apr(assetInfo.stakeApr ?: 0.0, iconUrl),
155152
assetInfo.lockTime?.let { StakeInfoRow.LockTime(it, iconUrl) },
153+
minAmountValue.takeIf { it > 0 }?.let { StakeInfoRow.MinAmount(it, assetInfo.asset.chain) },
156154
)
157155
itemsPositioned(rows) { position, row ->
158156
when (row) {

android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/DelegationItem.kt

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.gemwallet.android.ui.components.list_item
22

33
import androidx.compose.foundation.clickable
4-
import androidx.compose.foundation.layout.Column
54
import androidx.compose.foundation.layout.Row
65
import androidx.compose.material3.MaterialTheme
76
import androidx.compose.runtime.Composable
@@ -29,7 +28,6 @@ import com.wallet.core.primitives.DelegationState.Pending
2928
fun DelegationItem(
3029
assetInfo: AssetInfo,
3130
delegation: Delegation,
32-
completedAt: String,
3331
listPosition: ListPosition,
3432
onClick: () -> Unit
3533
) {
@@ -46,21 +44,7 @@ fun DelegationItem(
4644
ListItemTitleText(text = delegation.validator.name)
4745
},
4846
subtitle = {
49-
val stateColor = delegation.base.state.color()
50-
val stateText = delegation.stateText()
51-
Column {
52-
ListItemSupportText(stateText, color = stateColor)
53-
when (delegation.base.state) {
54-
Pending,
55-
Activating,
56-
Deactivating -> completedAt.takeIf { it.isNotEmpty() && it != "0" }?.let {
57-
ListItemSupportText(it)
58-
}
59-
Active,
60-
Inactive,
61-
AwaitingWithdrawal -> Unit
62-
}
63-
}
47+
ListItemSupportText(delegation.stateText(), color = delegation.base.state.color())
6448
},
6549
trailing = {
6650
val balance = DelegationBalanceInfoUIModel(

core/crates/gem_hypercore/src/provider/staking.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use async_trait::async_trait;
22
use chain_traits::ChainStaking;
3+
use chrono::Utc;
34
use futures::try_join;
45
use std::error::Error;
56

@@ -22,7 +23,7 @@ impl<C: Client> ChainStaking for HyperCoreClient<C> {
2223
}
2324

2425
async fn get_staking_delegations(&self, address: String) -> Result<Vec<DelegationBase>, Box<dyn Error + Sync + Send>> {
25-
let (delegations, stake_balance) = try_join!(self.get_staking_delegations(&address), self.get_stake_balance(&address))?;
26-
Ok(staking_mapper::map_staking_delegations(delegations, stake_balance, self.chain))
26+
let (delegations, history) = try_join!(self.get_staking_delegations(&address), self.get_delegator_history(&address))?;
27+
Ok(staking_mapper::map_staking_delegations(delegations, history, Utc::now(), self.chain))
2728
}
2829
}

core/crates/gem_hypercore/src/provider/staking_mapper.rs

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::models::balance::{DelegationBalance, StakeBalance, Validator};
1+
use crate::models::balance::{DelegationBalance, Validator};
2+
use crate::models::user::DelegatorHistoryUpdate;
3+
use crate::provider::transaction_state_mapper::DELEGATOR_WITHDRAWAL_INITIATED;
4+
use chrono::{DateTime, Duration, Utc};
25
use num_bigint::BigUint;
36
use number_formatter::BigNumberFormatter;
47
use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator};
@@ -15,7 +18,7 @@ pub fn map_staking_validators(validators: Vec<Validator>, chain: Chain, apy: Opt
1518
result
1619
}
1720

18-
pub fn map_staking_delegations(delegations: Vec<DelegationBalance>, stake_balance: StakeBalance, chain: Chain) -> Vec<DelegationBase> {
21+
pub fn map_staking_delegations(delegations: Vec<DelegationBalance>, history: Vec<DelegatorHistoryUpdate>, now: DateTime<Utc>, chain: Chain) -> Vec<DelegationBase> {
1922
let native_decimals = Asset::from_chain(chain).decimals as u32;
2023
let mut result: Vec<DelegationBase> = delegations
2124
.into_iter()
@@ -31,34 +34,57 @@ pub fn map_staking_delegations(delegations: Vec<DelegationBalance>, stake_balanc
3134
})
3235
.collect();
3336

34-
let pending = BigNumberFormatter::value_from_amount_biguint(&stake_balance.total_pending_withdrawal, native_decimals).unwrap_or_default();
35-
if pending > BigUint::from(0u32) {
36-
result.push(DelegationBase {
37-
asset_id: chain.as_asset_id(),
38-
state: DelegationState::Pending,
39-
balance: pending,
40-
shares: BigUint::from(0u32),
41-
rewards: BigUint::from(0u32),
42-
completion_date: None,
43-
delegation_id: DelegationValidator::SYSTEM_ID.to_string(),
44-
validator_id: DelegationValidator::SYSTEM_ID.to_string(),
45-
});
46-
}
47-
37+
result.extend(map_pending_withdrawals(history, now, chain, native_decimals));
4838
result
4939
}
5040

41+
fn map_pending_withdrawals(history: Vec<DelegatorHistoryUpdate>, now: DateTime<Utc>, chain: Chain, native_decimals: u32) -> Vec<DelegationBase> {
42+
let lock = Duration::seconds(chain.config().stake.as_ref().map(|stake| stake.lock_time).unwrap_or_default() as i64);
43+
44+
history
45+
.into_iter()
46+
.filter_map(|entry| {
47+
let withdrawal = entry.delta.withdrawal?;
48+
if withdrawal.phase != DELEGATOR_WITHDRAWAL_INITIATED {
49+
return None;
50+
}
51+
let completion_date = DateTime::from_timestamp_millis(entry.time as i64)? + lock;
52+
if completion_date <= now {
53+
return None;
54+
}
55+
Some(DelegationBase {
56+
asset_id: chain.as_asset_id(),
57+
state: DelegationState::Pending,
58+
balance: BigNumberFormatter::value_from_amount_biguint(&withdrawal.amount, native_decimals).unwrap_or_default(),
59+
shares: BigUint::from(0u32),
60+
rewards: BigUint::from(0u32),
61+
completion_date: Some(completion_date),
62+
delegation_id: format!("unstaking_{}", entry.time),
63+
validator_id: DelegationValidator::SYSTEM_ID.to_string(),
64+
})
65+
})
66+
.collect()
67+
}
68+
5169
#[cfg(test)]
5270
mod tests {
5371
use super::*;
5472
use crate::models::balance::ValidatorStats;
73+
use crate::models::user::{DelegatorHistoryDelta, DelegatorWithdrawalDelta};
5574
use primitives::{Chain, DelegationState};
5675

57-
fn stake_balance(total_pending_withdrawal: &str) -> StakeBalance {
58-
StakeBalance {
59-
delegated: "0".to_string(),
60-
undelegated: "0".to_string(),
61-
total_pending_withdrawal: total_pending_withdrawal.to_string(),
76+
fn withdrawal_entry(time: u64, amount: &str, phase: &str) -> DelegatorHistoryUpdate {
77+
DelegatorHistoryUpdate {
78+
time,
79+
hash: "0x0".to_string(),
80+
delta: DelegatorHistoryDelta {
81+
c_deposit: None,
82+
delegate: None,
83+
withdrawal: Some(DelegatorWithdrawalDelta {
84+
amount: amount.to_string(),
85+
phase: phase.to_string(),
86+
}),
87+
},
6288
}
6389
}
6490

@@ -107,7 +133,7 @@ mod tests {
107133
fn test_map_staking_delegations() {
108134
let delegations: Vec<DelegationBalance> = serde_json::from_str(include_str!("../../testdata/staking_delegations.json")).unwrap();
109135

110-
let result = map_staking_delegations(delegations, stake_balance("0"), Chain::HyperCore);
136+
let result = map_staking_delegations(delegations, vec![], Utc::now(), Chain::HyperCore);
111137

112138
assert_eq!(result.len(), 2);
113139

@@ -127,21 +153,33 @@ mod tests {
127153
}
128154

129155
#[test]
130-
fn test_map_staking_delegations_pending_withdrawal() {
131-
let result = map_staking_delegations(vec![], stake_balance("0.015"), Chain::HyperCore);
156+
fn test_map_staking_delegations_pending_withdrawals() {
157+
let now = DateTime::from_timestamp(1_780_000_000, 0).unwrap();
158+
let at = |days: i64| (now - Duration::days(days)).timestamp_millis() as u64;
159+
let history = vec![
160+
withdrawal_entry(at(1), "1.5", "initiated"),
161+
withdrawal_entry(at(8), "2.0", "initiated"),
162+
withdrawal_entry(at(1), "3.0", "finalized"),
163+
DelegatorHistoryUpdate {
164+
time: at(0),
165+
hash: "0x0".to_string(),
166+
delta: DelegatorHistoryDelta {
167+
c_deposit: None,
168+
delegate: None,
169+
withdrawal: None,
170+
},
171+
},
172+
];
173+
174+
let result = map_staking_delegations(vec![], history, now, Chain::HyperCore);
132175

133176
assert_eq!(result.len(), 1);
134177
let pending = &result[0];
135178
assert_eq!(pending.state, DelegationState::Pending);
136179
assert_eq!(pending.validator_id, DelegationValidator::SYSTEM_ID);
137-
assert_eq!(pending.balance.to_string(), "1500000");
138-
assert!(pending.completion_date.is_none());
139-
}
140-
141-
#[test]
142-
fn test_map_staking_delegations_no_pending_withdrawal() {
143-
let result = map_staking_delegations(vec![], stake_balance("0"), Chain::HyperCore);
144-
145-
assert!(result.is_empty());
180+
assert_eq!(pending.balance.to_string(), "150000000");
181+
assert_eq!(pending.delegation_id, format!("unstaking_{}", at(1)));
182+
assert_eq!(pending.completion_date, Some(now + Duration::days(6)));
183+
assert!(pending.completion_date.unwrap() > now);
146184
}
147185
}

core/crates/gem_hypercore/src/provider/transaction_state_mapper.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::perpetual_formatter::usdc_value;
1313

1414
pub const ACTION_HISTORY_QUERY_LOOKBACK_MS: u64 = 5_000;
1515
const ACTION_HISTORY_MATCH_WINDOW_MS: u64 = 5 * 60 * 1_000;
16-
const DELEGATOR_WITHDRAWAL_INITIATED: &str = "initiated";
16+
pub(crate) const DELEGATOR_WITHDRAWAL_INITIATED: &str = "initiated";
1717

1818
fn perpetual_fill_type_and_direction(dir: &FillDirection) -> Option<(TransactionType, PerpetualDirection)> {
1919
match dir {

ios/Packages/Store/Sources/Requests/DelegationsRequest.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c). Gem Wallet. All rights reserved.
22

3+
internal import BigInt
34
import Foundation
45
import GRDB
56
import Primitives
@@ -23,9 +24,9 @@ public struct DelegationsRequest: DatabaseQueryable {
2324
.filter(StakeDelegationRecord.Columns.assetId == assetId.identifier)
2425
.joining(required: StakeDelegationRecord.validator
2526
.filter(StakeValidatorRecord.Columns.providerType == providerType.rawValue))
26-
.order(StakeDelegationRecord.Columns.balance.desc)
2727
.asRequest(of: StakeDelegationInfo.self)
2828
.fetchAll(db)
2929
.compactMap { $0.mapToDelegation() }
30+
.sorted { $0.base.balanceValue > $1.base.balanceValue }
3031
}
3132
}

0 commit comments

Comments
 (0)