Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit 0c1a85b

Browse files
authored
Hyperliquid: fix partial unstake and show pending unstaking (#1166)
1 parent f55b25b commit 0c1a85b

8 files changed

Lines changed: 112 additions & 72 deletions

File tree

crates/gem_hypercore/src/models/balance.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,16 @@ pub struct Token {
3434
#[derive(Debug, Clone, Serialize, Deserialize)]
3535
#[serde(rename_all = "camelCase")]
3636
pub struct StakeBalance {
37-
#[serde(deserialize_with = "deserialize_f64_from_str")]
38-
pub delegated: f64,
39-
#[serde(deserialize_with = "deserialize_f64_from_str")]
40-
pub undelegated: f64,
41-
#[serde(deserialize_with = "deserialize_f64_from_str")]
42-
pub total_pending_withdrawal: f64,
37+
pub delegated: String,
38+
pub undelegated: String,
39+
pub total_pending_withdrawal: String,
4340
}
4441

4542
#[derive(Debug, Clone, Serialize, Deserialize)]
4643
#[serde(rename_all = "camelCase")]
4744
pub struct DelegationBalance {
4845
pub validator: String,
49-
#[serde(deserialize_with = "deserialize_f64_from_str")]
50-
pub amount: f64,
46+
pub amount: String,
5147
pub locked_until_timestamp: u64,
5248
}
5349

crates/gem_hypercore/src/provider/balances_mapper.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ pub fn map_balance_tokens(spot_balances: &Balances, spot_tokens: &[SpotToken], t
4747

4848
pub fn map_balance_staking(balance: &StakeBalance, chain: Chain) -> Result<AssetBalance, Box<dyn Error + Sync + Send>> {
4949
let native_decimals = Asset::from_chain(chain).decimals as u32;
50-
let available_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.delegated.to_string(), native_decimals).unwrap_or_default();
51-
let pending_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.total_pending_withdrawal.to_string(), native_decimals).unwrap_or_default();
50+
let available_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.delegated, native_decimals).unwrap_or_default();
51+
let pending_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.total_pending_withdrawal, native_decimals).unwrap_or_default();
5252

5353
Ok(AssetBalance::new_balance(
5454
chain.as_asset_id(),
@@ -138,9 +138,9 @@ mod tests {
138138
#[test]
139139
fn test_map_balance_staking() {
140140
let stake_balance = StakeBalance {
141-
delegated: 100.0,
142-
undelegated: 0.0,
143-
total_pending_withdrawal: 10.0,
141+
delegated: "100.0".to_string(),
142+
undelegated: "0.0".to_string(),
143+
total_pending_withdrawal: "10.0".to_string(),
144144
};
145145
let result = map_balance_staking(&stake_balance, Chain::HyperCore).unwrap();
146146

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 futures::try_join;
34
use std::error::Error;
45

56
use gem_client::Client;
@@ -21,7 +22,7 @@ impl<C: Client> ChainStaking for HyperCoreClient<C> {
2122
}
2223

2324
async fn get_staking_delegations(&self, address: String) -> Result<Vec<DelegationBase>, Box<dyn Error + Sync + Send>> {
24-
let delegations = self.get_staking_delegations(&address).await?;
25-
Ok(staking_mapper::map_staking_delegations(delegations, self.chain))
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))
2627
}
2728
}
Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,51 @@
1-
use crate::models::balance::{DelegationBalance, Validator};
1+
use crate::models::balance::{DelegationBalance, StakeBalance, Validator};
22
use num_bigint::BigUint;
33
use number_formatter::BigNumberFormatter;
44
use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator};
5-
use std::str::FromStr;
65

76
pub fn map_staking_validators(validators: Vec<Validator>, chain: Chain, apy: Option<f64>) -> Vec<DelegationValidator> {
87
let calculated_apy = apy.unwrap_or_else(|| Validator::max_apr(validators.clone()));
9-
validators
8+
let mut result: Vec<DelegationValidator> = validators
109
.into_iter()
1110
.map(|x| DelegationValidator::stake(chain, x.validator_address(), x.name, x.is_active, x.commission, calculated_apy))
12-
.collect()
11+
.collect();
12+
13+
result.push(DelegationValidator::system(chain));
14+
15+
result
1316
}
1417

15-
pub fn map_staking_delegations(delegations: Vec<DelegationBalance>, chain: Chain) -> Vec<DelegationBase> {
18+
pub fn map_staking_delegations(delegations: Vec<DelegationBalance>, stake_balance: StakeBalance, chain: Chain) -> Vec<DelegationBase> {
1619
let native_decimals = Asset::from_chain(chain).decimals as u32;
17-
delegations
20+
let mut result: Vec<DelegationBase> = delegations
1821
.into_iter()
19-
.map(|x| {
20-
let balance = BigNumberFormatter::value_from_amount(&x.amount.to_string(), native_decimals)
21-
.ok()
22-
.and_then(|s| BigUint::from_str(&s).ok())
23-
.unwrap_or_default();
24-
DelegationBase {
25-
asset_id: chain.as_asset_id(),
26-
state: DelegationState::Active,
27-
balance,
28-
shares: BigUint::from(0u32),
29-
rewards: BigUint::from(0u32),
30-
completion_date: None,
31-
delegation_id: x.validator_address(),
32-
validator_id: x.validator_address(),
33-
}
22+
.map(|x| DelegationBase {
23+
asset_id: chain.as_asset_id(),
24+
state: DelegationState::Active,
25+
balance: BigNumberFormatter::value_from_amount_biguint(&x.amount, native_decimals).unwrap_or_default(),
26+
shares: BigUint::from(0u32),
27+
rewards: BigUint::from(0u32),
28+
completion_date: None,
29+
delegation_id: x.validator_address(),
30+
validator_id: x.validator_address(),
3431
})
35-
.collect()
32+
.collect();
33+
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+
48+
result
3649
}
3750

3851
#[cfg(test)]
@@ -41,6 +54,14 @@ mod tests {
4154
use crate::models::balance::ValidatorStats;
4255
use primitives::{Chain, DelegationState};
4356

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(),
62+
}
63+
}
64+
4465
#[test]
4566
fn test_map_staking_validators() {
4667
let validators = vec![Validator {
@@ -52,13 +73,18 @@ mod tests {
5273
}];
5374

5475
let result = map_staking_validators(validators, Chain::HyperCore, None);
55-
assert_eq!(result.len(), 1);
76+
assert_eq!(result.len(), 2);
5677
assert_eq!(result[0].name, "Test Validator");
5778
assert_eq!(result[0].id, "0x5aC99df645F3414876C816Caa18b2d234024b487");
5879
assert_eq!(result[0].chain, Chain::HyperCore);
5980
assert!(result[0].is_active);
6081
assert_eq!(result[0].commission, 5.0);
61-
assert_eq!(result[0].apr, 15.0); // max_apr * 100
82+
assert_eq!(result[0].apr, 15.0);
83+
84+
let system = &result[1];
85+
assert_eq!(system.id, DelegationValidator::SYSTEM_ID);
86+
assert_eq!(system.name, DelegationValidator::SYSTEM_NAME);
87+
assert!(system.is_active);
6288
}
6389

6490
#[test]
@@ -72,15 +98,16 @@ mod tests {
7298
}];
7399

74100
let result = map_staking_validators(validators, Chain::HyperCore, Some(10.0));
75-
assert_eq!(result.len(), 1);
76-
assert_eq!(result[0].apr, 10.0); // Uses provided APY
101+
assert_eq!(result.len(), 2);
102+
assert_eq!(result[0].apr, 10.0);
103+
assert_eq!(result[1].id, DelegationValidator::SYSTEM_ID);
77104
}
78105

79106
#[test]
80107
fn test_map_staking_delegations() {
81108
let delegations: Vec<DelegationBalance> = serde_json::from_str(include_str!("../../testdata/staking_delegations.json")).unwrap();
82109

83-
let result = map_staking_delegations(delegations, Chain::HyperCore);
110+
let result = map_staking_delegations(delegations, stake_balance("0"), Chain::HyperCore);
84111

85112
assert_eq!(result.len(), 2);
86113

@@ -89,7 +116,7 @@ mod tests {
89116
assert_eq!(delegation1.validator_id, "0x5aC99df645F3414876C816Caa18b2d234024b487");
90117
assert_eq!(delegation1.delegation_id, "0x5aC99df645F3414876C816Caa18b2d234024b487");
91118
assert_eq!(delegation1.balance.to_string(), "271936493373");
92-
assert!(matches!(delegation1.state, DelegationState::Active));
119+
assert_eq!(delegation1.state, DelegationState::Active);
93120
assert_eq!(delegation1.shares, num_bigint::BigUint::from(0u32));
94121
assert_eq!(delegation1.rewards, num_bigint::BigUint::from(0u32));
95122
assert!(delegation1.completion_date.is_none());
@@ -98,4 +125,23 @@ mod tests {
98125
assert_eq!(delegation2.validator_id, "0xaBCDefF4b3727B83A23697500EEf089020DF2cD2");
99126
assert_eq!(delegation2.balance.to_string(), "1814578086");
100127
}
128+
129+
#[test]
130+
fn test_map_staking_delegations_pending_withdrawal() {
131+
let result = map_staking_delegations(vec![], stake_balance("0.015"), Chain::HyperCore);
132+
133+
assert_eq!(result.len(), 1);
134+
let pending = &result[0];
135+
assert_eq!(pending.state, DelegationState::Pending);
136+
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());
146+
}
101147
}

crates/gem_hypercore/src/signer/core_signer.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ impl HyperCoreSigner {
103103
Ok(vec![deposit_action, delegate_action])
104104
}
105105
StakeType::Unstake(delegation) => {
106-
let balance = delegation.base.balance.to_string();
107-
let wei = BigNumberFormatter::value_as_u64(&balance, 0).map_err(|err| SignerError::InvalidInput(err.to_string()))?;
106+
let wei = BigNumberFormatter::value_as_u64(&input.value, 0).map_err(|err| SignerError::InvalidInput(err.to_string()))?;
108107

109108
let undelegate_request = TokenDelegate::new(delegation.validator.id.clone(), wei, true, nonce_incrementer.next_val());
110109
let undelegate_action = self.sign_token_delegate(undelegate_request, private_key)?;
@@ -438,7 +437,7 @@ mod tests {
438437
}
439438

440439
#[test]
441-
fn unstake_actions_have_unique_nonces() {
440+
fn unstake_uses_entered_amount_and_unique_nonces() {
442441
let signer = HyperCoreSigner;
443442
let asset = Asset::from_chain(Chain::HyperCore);
444443
let delegation = Delegation {
@@ -456,7 +455,7 @@ mod tests {
456455
price: None,
457456
};
458457
let input = TransactionLoadInput {
459-
value: "0".into(),
458+
value: "60000000".into(),
460459
sender_address: "0xsender".into(),
461460
destination_address: "".into(),
462461
..TransactionLoadInput::mock_with_input_type(TransactionInputType::Stake(asset, StakeType::Unstake(delegation)))
@@ -467,16 +466,19 @@ mod tests {
467466
let responses = signer.sign_stake_action(&input, &private_key).expect("should sign");
468467
assert_eq!(responses.len(), 2);
469468

470-
let nonces: Vec<u64> = responses
471-
.iter()
472-
.map(|payload| {
473-
let value: serde_json::Value = serde_json::from_str(payload).expect("valid json");
474-
value["action"]["nonce"].as_u64().expect("action nonce")
475-
})
476-
.collect();
469+
let undelegate: serde_json::Value = serde_json::from_str(&responses[0]).expect("json");
470+
let withdraw: serde_json::Value = serde_json::from_str(&responses[1]).expect("json");
471+
472+
assert_eq!(undelegate["action"]["type"], "tokenDelegate");
473+
assert_eq!(undelegate["action"]["isUndelegate"], true);
474+
assert_eq!(withdraw["action"]["type"], "cWithdraw");
475+
476+
assert_eq!(undelegate["action"]["wei"].as_u64().expect("undelegate wei"), 60000000);
477+
assert_eq!(withdraw["action"]["wei"].as_u64().expect("withdraw wei"), 60000000);
477478

478-
assert_eq!(nonces.len(), 2);
479-
assert!(nonces[0] < nonces[1], "unstake actions should advance nonce");
479+
let undelegate_nonce = undelegate["action"]["nonce"].as_u64().expect("nonce");
480+
let withdraw_nonce = withdraw["action"]["nonce"].as_u64().expect("nonce");
481+
assert!(undelegate_nonce < withdraw_nonce, "unstake actions should advance nonce");
480482
}
481483

482484
#[test]

crates/gem_tron/src/provider/staking.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ use super::staking_mapper::map_staking_validators;
1111
use crate::rpc::client::TronClient;
1212
use crate::rpc::constants::{GET_WITNESS_127_PAY_PER_BLOCK, GET_WITNESS_PAY_PER_BLOCK};
1313

14-
const SYSTEM_VALIDATOR_ID: &str = "system";
15-
1614
#[async_trait]
1715
impl<C: Client + Clone> ChainStaking for TronClient<C> {
1816
async fn get_staking_apy(&self) -> Result<Option<f64>, Box<dyn Error + Sync + Send>> {
@@ -70,7 +68,7 @@ impl<C: Client + Clone> ChainStaking for TronClient<C> {
7068
rewards: BigUint::from(0u32),
7169
completion_date: Some(completion_date),
7270
delegation_id: completion_date.timestamp().to_string(),
73-
validator_id: SYSTEM_VALIDATOR_ID.to_string(),
71+
validator_id: DelegationValidator::SYSTEM_ID.to_string(),
7472
});
7573
}
7674
}

crates/gem_tron/src/provider/staking_mapper.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ use crate::address::TronAddress;
22
use crate::models::WitnessesList;
33
use primitives::{Address as _, Chain, DelegationValidator, StakeValidator};
44

5-
const SYSTEM_UNSTAKING_VALIDATOR_ID: &str = "system";
6-
const SYSTEM_UNSTAKING_VALIDATOR_NAME: &str = "Unstaking";
7-
85
pub fn map_validators(witnesses: WitnessesList) -> Vec<StakeValidator> {
96
witnesses.witnesses.into_iter().map(|x| StakeValidator::new(x.address, x.url)).collect()
107
}
@@ -26,14 +23,7 @@ pub fn map_staking_validators(witnesses: WitnessesList, apy: Option<f64>) -> Vec
2623
})
2724
.collect();
2825

29-
validators.push(DelegationValidator::stake(
30-
Chain::Tron,
31-
SYSTEM_UNSTAKING_VALIDATOR_ID.to_string(),
32-
SYSTEM_UNSTAKING_VALIDATOR_NAME.to_string(),
33-
true,
34-
0.0,
35-
default_apy,
36-
));
26+
validators.push(DelegationValidator::system(Chain::Tron));
3727

3828
validators
3929
}
@@ -79,8 +69,8 @@ mod tests {
7969
assert_eq!(validators[1].id, "TEqyWRKCzREYC2bK2fc3j7pp8XjAa6tJK1");
8070
assert!(!validators[1].is_active);
8171

82-
assert_eq!(validators[2].id, SYSTEM_UNSTAKING_VALIDATOR_ID);
83-
assert_eq!(validators[2].name, SYSTEM_UNSTAKING_VALIDATOR_NAME);
72+
assert_eq!(validators[2].id, DelegationValidator::SYSTEM_ID);
73+
assert_eq!(validators[2].name, DelegationValidator::SYSTEM_NAME);
8474
assert!(validators[2].is_active);
8575
}
8676
}

crates/primitives/src/delegation.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ pub struct DelegationValidator {
6666
}
6767

6868
impl DelegationValidator {
69+
pub const SYSTEM_ID: &str = "system";
70+
pub const SYSTEM_NAME: &str = "Unstaking";
71+
6972
pub fn stake(chain: Chain, id: String, name: String, is_active: bool, commission: f64, apr: f64) -> Self {
7073
Self {
7174
chain,
@@ -77,6 +80,10 @@ impl DelegationValidator {
7780
provider_type: StakeProviderType::Stake,
7881
}
7982
}
83+
84+
pub fn system(chain: Chain) -> Self {
85+
Self::stake(chain, Self::SYSTEM_ID.to_string(), Self::SYSTEM_NAME.to_string(), true, 0.0, 0.0)
86+
}
8087
}
8188

8289
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq)]

0 commit comments

Comments
 (0)