From d3fe2056a3198b0933c8fae19eb48884ed11335a Mon Sep 17 00:00:00 2001 From: Radmir Date: Mon, 1 Jun 2026 23:05:01 +0500 Subject: [PATCH 1/2] 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. --- .../viewmodels/DelegationViewModel.kt | 2 +- .../features/stake/presents/StakeScene.kt | 4 +- .../ui/components/list_item/DelegationItem.kt | 18 +--- .../gem_hypercore/src/models/balance.rs | 24 +++++ .../gem_hypercore/src/provider/staking.rs | 5 +- .../src/provider/staking_mapper.rs | 95 ++++++++++++------- core/crates/gem_hypercore/src/rpc/client.rs | 6 +- .../Sources/Requests/DelegationsRequest.swift | 3 +- 8 files changed, 98 insertions(+), 59 deletions(-) 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/models/balance.rs b/core/crates/gem_hypercore/src/models/balance.rs index 3533293a29..cbe7fede46 100644 --- a/core/crates/gem_hypercore/src/models/balance.rs +++ b/core/crates/gem_hypercore/src/models/balance.rs @@ -53,6 +53,30 @@ impl DelegationBalance { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DelegatorHistoryEntry { + pub time: u64, + pub delta: DelegatorDelta, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DelegatorDelta { + pub withdrawal: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DelegatorWithdrawal { + pub amount: String, + pub phase: WithdrawalPhase, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum WithdrawalPhase { + Initiated, + Finalized, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Validator { diff --git a/core/crates/gem_hypercore/src/provider/staking.rs b/core/crates/gem_hypercore/src/provider/staking.rs index 69ef9de08b..f3344d7420 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_staking_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..c9698dbf2f 100644 --- a/core/crates/gem_hypercore/src/provider/staking_mapper.rs +++ b/core/crates/gem_hypercore/src/provider/staking_mapper.rs @@ -1,4 +1,5 @@ -use crate::models::balance::{DelegationBalance, StakeBalance, Validator}; +use crate::models::balance::{DelegationBalance, DelegatorHistoryEntry, Validator, WithdrawalPhase}; +use chrono::{DateTime, Duration, Utc}; use num_bigint::BigUint; use number_formatter::BigNumberFormatter; use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator}; @@ -15,7 +16,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 +32,53 @@ 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 != WithdrawalPhase::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::balance::{DelegatorDelta, DelegatorWithdrawal, ValidatorStats}; 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: WithdrawalPhase) -> DelegatorHistoryEntry { + DelegatorHistoryEntry { + time, + delta: DelegatorDelta { + withdrawal: Some(DelegatorWithdrawal { + amount: amount.to_string(), + phase, + }), + }, } } @@ -107,7 +127,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 +147,28 @@ 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", WithdrawalPhase::Initiated), + withdrawal_entry(at(8), "2.0", WithdrawalPhase::Initiated), + withdrawal_entry(at(1), "3.0", WithdrawalPhase::Finalized), + DelegatorHistoryEntry { + time: at(0), + delta: DelegatorDelta { 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/rpc/client.rs b/core/crates/gem_hypercore/src/rpc/client.rs index bc82014e63..08752c3ade 100644 --- a/core/crates/gem_hypercore/src/rpc/client.rs +++ b/core/crates/gem_hypercore/src/rpc/client.rs @@ -1,5 +1,5 @@ use crate::models::{ - balance::{Balances, DelegationBalance, StakeBalance, Validator}, + balance::{Balances, DelegationBalance, DelegatorHistoryEntry, StakeBalance, Validator}, candlestick::Candlestick, metadata::HypercoreMetadataResponse, order::{OpenOrder, UserFill}, @@ -115,6 +115,10 @@ impl HyperCoreClient { self.info(json!({"type": "delegations", "user": user})).await } + pub async fn get_staking_history(&self, user: &str) -> Result, Box> { + self.info(json!({"type": "delegatorHistory", "user": user})).await + } + pub async fn get_staking_apy(&self) -> Result> { let validators = self.get_validators().await?; Ok(Validator::max_apr(validators)) 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 } } } From edeb9a5620acd3cb9761ac013d1f86d0e4a3a6ae Mon Sep 17 00:00:00 2001 From: Radmir Date: Tue, 2 Jun 2026 12:13:36 +0500 Subject: [PATCH 2/2] 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. --- .../gem_hypercore/src/models/balance.rs | 24 ----------- .../gem_hypercore/src/provider/staking.rs | 2 +- .../src/provider/staking_mapper.rs | 41 ++++++++++++------- .../src/provider/transaction_state_mapper.rs | 2 +- core/crates/gem_hypercore/src/rpc/client.rs | 6 +-- 5 files changed, 29 insertions(+), 46 deletions(-) diff --git a/core/crates/gem_hypercore/src/models/balance.rs b/core/crates/gem_hypercore/src/models/balance.rs index cbe7fede46..3533293a29 100644 --- a/core/crates/gem_hypercore/src/models/balance.rs +++ b/core/crates/gem_hypercore/src/models/balance.rs @@ -53,30 +53,6 @@ impl DelegationBalance { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DelegatorHistoryEntry { - pub time: u64, - pub delta: DelegatorDelta, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DelegatorDelta { - pub withdrawal: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DelegatorWithdrawal { - pub amount: String, - pub phase: WithdrawalPhase, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum WithdrawalPhase { - Initiated, - Finalized, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Validator { diff --git a/core/crates/gem_hypercore/src/provider/staking.rs b/core/crates/gem_hypercore/src/provider/staking.rs index f3344d7420..1a48ba075c 100644 --- a/core/crates/gem_hypercore/src/provider/staking.rs +++ b/core/crates/gem_hypercore/src/provider/staking.rs @@ -23,7 +23,7 @@ impl ChainStaking for HyperCoreClient { } async fn get_staking_delegations(&self, address: String) -> Result, Box> { - let (delegations, history) = try_join!(self.get_staking_delegations(&address), self.get_staking_history(&address))?; + 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 c9698dbf2f..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,6 @@ -use crate::models::balance::{DelegationBalance, DelegatorHistoryEntry, Validator, WithdrawalPhase}; +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; @@ -16,7 +18,7 @@ pub fn map_staking_validators(validators: Vec, chain: Chain, apy: Opt result } -pub fn map_staking_delegations(delegations: Vec, history: Vec, now: DateTime, 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() @@ -36,14 +38,14 @@ pub fn map_staking_delegations(delegations: Vec, history: Vec result } -fn map_pending_withdrawals(history: Vec, now: DateTime, chain: Chain, native_decimals: u32) -> Vec { +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 != WithdrawalPhase::Initiated { + if withdrawal.phase != DELEGATOR_WITHDRAWAL_INITIATED { return None; } let completion_date = DateTime::from_timestamp_millis(entry.time as i64)? + lock; @@ -67,16 +69,20 @@ fn map_pending_withdrawals(history: Vec, now: DateTime DelegatorHistoryEntry { - DelegatorHistoryEntry { + fn withdrawal_entry(time: u64, amount: &str, phase: &str) -> DelegatorHistoryUpdate { + DelegatorHistoryUpdate { time, - delta: DelegatorDelta { - withdrawal: Some(DelegatorWithdrawal { + hash: "0x0".to_string(), + delta: DelegatorHistoryDelta { + c_deposit: None, + delegate: None, + withdrawal: Some(DelegatorWithdrawalDelta { amount: amount.to_string(), - phase, + phase: phase.to_string(), }), }, } @@ -151,12 +157,17 @@ mod tests { 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", WithdrawalPhase::Initiated), - withdrawal_entry(at(8), "2.0", WithdrawalPhase::Initiated), - withdrawal_entry(at(1), "3.0", WithdrawalPhase::Finalized), - DelegatorHistoryEntry { + 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), - delta: DelegatorDelta { withdrawal: None }, + hash: "0x0".to_string(), + delta: DelegatorHistoryDelta { + c_deposit: None, + delegate: None, + withdrawal: None, + }, }, ]; 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/core/crates/gem_hypercore/src/rpc/client.rs b/core/crates/gem_hypercore/src/rpc/client.rs index 08752c3ade..bc82014e63 100644 --- a/core/crates/gem_hypercore/src/rpc/client.rs +++ b/core/crates/gem_hypercore/src/rpc/client.rs @@ -1,5 +1,5 @@ use crate::models::{ - balance::{Balances, DelegationBalance, DelegatorHistoryEntry, StakeBalance, Validator}, + balance::{Balances, DelegationBalance, StakeBalance, Validator}, candlestick::Candlestick, metadata::HypercoreMetadataResponse, order::{OpenOrder, UserFill}, @@ -115,10 +115,6 @@ impl HyperCoreClient { self.info(json!({"type": "delegations", "user": user})).await } - pub async fn get_staking_history(&self, user: &str) -> Result, Box> { - self.info(json!({"type": "delegatorHistory", "user": user})).await - } - pub async fn get_staking_apy(&self) -> Result> { let validators = self.get_validators().await?; Ok(Validator::max_apr(validators))