diff --git a/crates/gem_hypercore/src/models/balance.rs b/crates/gem_hypercore/src/models/balance.rs index 81509d582..3533293a2 100644 --- a/crates/gem_hypercore/src/models/balance.rs +++ b/crates/gem_hypercore/src/models/balance.rs @@ -34,20 +34,16 @@ pub struct Token { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StakeBalance { - #[serde(deserialize_with = "deserialize_f64_from_str")] - pub delegated: f64, - #[serde(deserialize_with = "deserialize_f64_from_str")] - pub undelegated: f64, - #[serde(deserialize_with = "deserialize_f64_from_str")] - pub total_pending_withdrawal: f64, + pub delegated: String, + pub undelegated: String, + pub total_pending_withdrawal: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DelegationBalance { pub validator: String, - #[serde(deserialize_with = "deserialize_f64_from_str")] - pub amount: f64, + pub amount: String, pub locked_until_timestamp: u64, } diff --git a/crates/gem_hypercore/src/provider/balances_mapper.rs b/crates/gem_hypercore/src/provider/balances_mapper.rs index e41af81c6..11c49245c 100644 --- a/crates/gem_hypercore/src/provider/balances_mapper.rs +++ b/crates/gem_hypercore/src/provider/balances_mapper.rs @@ -47,8 +47,8 @@ pub fn map_balance_tokens(spot_balances: &Balances, spot_tokens: &[SpotToken], t pub fn map_balance_staking(balance: &StakeBalance, chain: Chain) -> Result> { let native_decimals = Asset::from_chain(chain).decimals as u32; - let available_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.delegated.to_string(), native_decimals).unwrap_or_default(); - let pending_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.total_pending_withdrawal.to_string(), native_decimals).unwrap_or_default(); + let available_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.delegated, native_decimals).unwrap_or_default(); + let pending_biguint = BigNumberFormatter::value_from_amount_biguint(&balance.total_pending_withdrawal, native_decimals).unwrap_or_default(); Ok(AssetBalance::new_balance( chain.as_asset_id(), @@ -138,9 +138,9 @@ mod tests { #[test] fn test_map_balance_staking() { let stake_balance = StakeBalance { - delegated: 100.0, - undelegated: 0.0, - total_pending_withdrawal: 10.0, + delegated: "100.0".to_string(), + undelegated: "0.0".to_string(), + total_pending_withdrawal: "10.0".to_string(), }; let result = map_balance_staking(&stake_balance, Chain::HyperCore).unwrap(); diff --git a/crates/gem_hypercore/src/provider/staking.rs b/crates/gem_hypercore/src/provider/staking.rs index fb052b8cb..69ef9de08 100644 --- a/crates/gem_hypercore/src/provider/staking.rs +++ b/crates/gem_hypercore/src/provider/staking.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use chain_traits::ChainStaking; +use futures::try_join; use std::error::Error; use gem_client::Client; @@ -21,7 +22,7 @@ impl ChainStaking for HyperCoreClient { } async fn get_staking_delegations(&self, address: String) -> Result, Box> { - let delegations = self.get_staking_delegations(&address).await?; - Ok(staking_mapper::map_staking_delegations(delegations, self.chain)) + 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)) } } diff --git a/crates/gem_hypercore/src/provider/staking_mapper.rs b/crates/gem_hypercore/src/provider/staking_mapper.rs index 646fa00a7..99489ffae 100644 --- a/crates/gem_hypercore/src/provider/staking_mapper.rs +++ b/crates/gem_hypercore/src/provider/staking_mapper.rs @@ -1,38 +1,51 @@ -use crate::models::balance::{DelegationBalance, Validator}; +use crate::models::balance::{DelegationBalance, StakeBalance, Validator}; use num_bigint::BigUint; use number_formatter::BigNumberFormatter; use primitives::{Asset, Chain, DelegationBase, DelegationState, DelegationValidator}; -use std::str::FromStr; pub fn map_staking_validators(validators: Vec, chain: Chain, apy: Option) -> Vec { let calculated_apy = apy.unwrap_or_else(|| Validator::max_apr(validators.clone())); - validators + let mut result: Vec = validators .into_iter() .map(|x| DelegationValidator::stake(chain, x.validator_address(), x.name, x.is_active, x.commission, calculated_apy)) - .collect() + .collect(); + + result.push(DelegationValidator::system(chain)); + + result } -pub fn map_staking_delegations(delegations: Vec, chain: Chain) -> Vec { +pub fn map_staking_delegations(delegations: Vec, stake_balance: StakeBalance, chain: Chain) -> Vec { let native_decimals = Asset::from_chain(chain).decimals as u32; - delegations + let mut result: Vec = delegations .into_iter() - .map(|x| { - let balance = BigNumberFormatter::value_from_amount(&x.amount.to_string(), native_decimals) - .ok() - .and_then(|s| BigUint::from_str(&s).ok()) - .unwrap_or_default(); - DelegationBase { - asset_id: chain.as_asset_id(), - state: DelegationState::Active, - balance, - shares: BigUint::from(0u32), - rewards: BigUint::from(0u32), - completion_date: None, - delegation_id: x.validator_address(), - validator_id: x.validator_address(), - } + .map(|x| DelegationBase { + asset_id: chain.as_asset_id(), + state: DelegationState::Active, + balance: BigNumberFormatter::value_from_amount_biguint(&x.amount, native_decimals).unwrap_or_default(), + shares: BigUint::from(0u32), + rewards: BigUint::from(0u32), + completion_date: None, + delegation_id: x.validator_address(), + validator_id: x.validator_address(), }) - .collect() + .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 } #[cfg(test)] @@ -41,6 +54,14 @@ mod tests { use crate::models::balance::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(), + } + } + #[test] fn test_map_staking_validators() { let validators = vec![Validator { @@ -52,13 +73,18 @@ mod tests { }]; let result = map_staking_validators(validators, Chain::HyperCore, None); - assert_eq!(result.len(), 1); + assert_eq!(result.len(), 2); assert_eq!(result[0].name, "Test Validator"); assert_eq!(result[0].id, "0x5aC99df645F3414876C816Caa18b2d234024b487"); assert_eq!(result[0].chain, Chain::HyperCore); assert!(result[0].is_active); assert_eq!(result[0].commission, 5.0); - assert_eq!(result[0].apr, 15.0); // max_apr * 100 + assert_eq!(result[0].apr, 15.0); + + let system = &result[1]; + assert_eq!(system.id, DelegationValidator::SYSTEM_ID); + assert_eq!(system.name, DelegationValidator::SYSTEM_NAME); + assert!(system.is_active); } #[test] @@ -72,15 +98,16 @@ mod tests { }]; let result = map_staking_validators(validators, Chain::HyperCore, Some(10.0)); - assert_eq!(result.len(), 1); - assert_eq!(result[0].apr, 10.0); // Uses provided APY + assert_eq!(result.len(), 2); + assert_eq!(result[0].apr, 10.0); + assert_eq!(result[1].id, DelegationValidator::SYSTEM_ID); } #[test] 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, Chain::HyperCore); + let result = map_staking_delegations(delegations, stake_balance("0"), Chain::HyperCore); assert_eq!(result.len(), 2); @@ -89,7 +116,7 @@ mod tests { assert_eq!(delegation1.validator_id, "0x5aC99df645F3414876C816Caa18b2d234024b487"); assert_eq!(delegation1.delegation_id, "0x5aC99df645F3414876C816Caa18b2d234024b487"); assert_eq!(delegation1.balance.to_string(), "271936493373"); - assert!(matches!(delegation1.state, DelegationState::Active)); + assert_eq!(delegation1.state, DelegationState::Active); assert_eq!(delegation1.shares, num_bigint::BigUint::from(0u32)); assert_eq!(delegation1.rewards, num_bigint::BigUint::from(0u32)); assert!(delegation1.completion_date.is_none()); @@ -98,4 +125,23 @@ mod tests { assert_eq!(delegation2.validator_id, "0xaBCDefF4b3727B83A23697500EEf089020DF2cD2"); assert_eq!(delegation2.balance.to_string(), "1814578086"); } + + #[test] + fn test_map_staking_delegations_pending_withdrawal() { + let result = map_staking_delegations(vec![], stake_balance("0.015"), 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()); + } } diff --git a/crates/gem_hypercore/src/signer/core_signer.rs b/crates/gem_hypercore/src/signer/core_signer.rs index 8ad718776..72cbebac9 100644 --- a/crates/gem_hypercore/src/signer/core_signer.rs +++ b/crates/gem_hypercore/src/signer/core_signer.rs @@ -103,8 +103,7 @@ impl HyperCoreSigner { Ok(vec![deposit_action, delegate_action]) } StakeType::Unstake(delegation) => { - let balance = delegation.base.balance.to_string(); - let wei = BigNumberFormatter::value_as_u64(&balance, 0).map_err(|err| SignerError::InvalidInput(err.to_string()))?; + let wei = BigNumberFormatter::value_as_u64(&input.value, 0).map_err(|err| SignerError::InvalidInput(err.to_string()))?; let undelegate_request = TokenDelegate::new(delegation.validator.id.clone(), wei, true, nonce_incrementer.next_val()); let undelegate_action = self.sign_token_delegate(undelegate_request, private_key)?; @@ -438,7 +437,7 @@ mod tests { } #[test] - fn unstake_actions_have_unique_nonces() { + fn unstake_uses_entered_amount_and_unique_nonces() { let signer = HyperCoreSigner; let asset = Asset::from_chain(Chain::HyperCore); let delegation = Delegation { @@ -456,7 +455,7 @@ mod tests { price: None, }; let input = TransactionLoadInput { - value: "0".into(), + value: "60000000".into(), sender_address: "0xsender".into(), destination_address: "".into(), ..TransactionLoadInput::mock_with_input_type(TransactionInputType::Stake(asset, StakeType::Unstake(delegation))) @@ -467,16 +466,19 @@ mod tests { let responses = signer.sign_stake_action(&input, &private_key).expect("should sign"); assert_eq!(responses.len(), 2); - let nonces: Vec = responses - .iter() - .map(|payload| { - let value: serde_json::Value = serde_json::from_str(payload).expect("valid json"); - value["action"]["nonce"].as_u64().expect("action nonce") - }) - .collect(); + let undelegate: serde_json::Value = serde_json::from_str(&responses[0]).expect("json"); + let withdraw: serde_json::Value = serde_json::from_str(&responses[1]).expect("json"); + + assert_eq!(undelegate["action"]["type"], "tokenDelegate"); + assert_eq!(undelegate["action"]["isUndelegate"], true); + assert_eq!(withdraw["action"]["type"], "cWithdraw"); + + assert_eq!(undelegate["action"]["wei"].as_u64().expect("undelegate wei"), 60000000); + assert_eq!(withdraw["action"]["wei"].as_u64().expect("withdraw wei"), 60000000); - assert_eq!(nonces.len(), 2); - assert!(nonces[0] < nonces[1], "unstake actions should advance nonce"); + let undelegate_nonce = undelegate["action"]["nonce"].as_u64().expect("nonce"); + let withdraw_nonce = withdraw["action"]["nonce"].as_u64().expect("nonce"); + assert!(undelegate_nonce < withdraw_nonce, "unstake actions should advance nonce"); } #[test] diff --git a/crates/gem_tron/src/provider/staking.rs b/crates/gem_tron/src/provider/staking.rs index a2365c2b5..e1b26ba88 100644 --- a/crates/gem_tron/src/provider/staking.rs +++ b/crates/gem_tron/src/provider/staking.rs @@ -11,8 +11,6 @@ use super::staking_mapper::map_staking_validators; use crate::rpc::client::TronClient; use crate::rpc::constants::{GET_WITNESS_127_PAY_PER_BLOCK, GET_WITNESS_PAY_PER_BLOCK}; -const SYSTEM_VALIDATOR_ID: &str = "system"; - #[async_trait] impl ChainStaking for TronClient { async fn get_staking_apy(&self) -> Result, Box> { @@ -70,7 +68,7 @@ impl ChainStaking for TronClient { rewards: BigUint::from(0u32), completion_date: Some(completion_date), delegation_id: completion_date.timestamp().to_string(), - validator_id: SYSTEM_VALIDATOR_ID.to_string(), + validator_id: DelegationValidator::SYSTEM_ID.to_string(), }); } } diff --git a/crates/gem_tron/src/provider/staking_mapper.rs b/crates/gem_tron/src/provider/staking_mapper.rs index 147ddf3b6..0c2816974 100644 --- a/crates/gem_tron/src/provider/staking_mapper.rs +++ b/crates/gem_tron/src/provider/staking_mapper.rs @@ -2,9 +2,6 @@ use crate::address::TronAddress; use crate::models::WitnessesList; use primitives::{Address as _, Chain, DelegationValidator, StakeValidator}; -const SYSTEM_UNSTAKING_VALIDATOR_ID: &str = "system"; -const SYSTEM_UNSTAKING_VALIDATOR_NAME: &str = "Unstaking"; - pub fn map_validators(witnesses: WitnessesList) -> Vec { witnesses.witnesses.into_iter().map(|x| StakeValidator::new(x.address, x.url)).collect() } @@ -26,14 +23,7 @@ pub fn map_staking_validators(witnesses: WitnessesList, apy: Option) -> Vec }) .collect(); - validators.push(DelegationValidator::stake( - Chain::Tron, - SYSTEM_UNSTAKING_VALIDATOR_ID.to_string(), - SYSTEM_UNSTAKING_VALIDATOR_NAME.to_string(), - true, - 0.0, - default_apy, - )); + validators.push(DelegationValidator::system(Chain::Tron)); validators } @@ -79,8 +69,8 @@ mod tests { assert_eq!(validators[1].id, "TEqyWRKCzREYC2bK2fc3j7pp8XjAa6tJK1"); assert!(!validators[1].is_active); - assert_eq!(validators[2].id, SYSTEM_UNSTAKING_VALIDATOR_ID); - assert_eq!(validators[2].name, SYSTEM_UNSTAKING_VALIDATOR_NAME); + assert_eq!(validators[2].id, DelegationValidator::SYSTEM_ID); + assert_eq!(validators[2].name, DelegationValidator::SYSTEM_NAME); assert!(validators[2].is_active); } } diff --git a/crates/primitives/src/delegation.rs b/crates/primitives/src/delegation.rs index 4f9a91e74..9f702aefa 100644 --- a/crates/primitives/src/delegation.rs +++ b/crates/primitives/src/delegation.rs @@ -66,6 +66,9 @@ pub struct DelegationValidator { } impl DelegationValidator { + pub const SYSTEM_ID: &str = "system"; + pub const SYSTEM_NAME: &str = "Unstaking"; + pub fn stake(chain: Chain, id: String, name: String, is_active: bool, commission: f64, apr: f64) -> Self { Self { chain, @@ -77,6 +80,10 @@ impl DelegationValidator { provider_type: StakeProviderType::Stake, } } + + pub fn system(chain: Chain) -> Self { + Self::stake(chain, Self::SYSTEM_ID.to_string(), Self::SYSTEM_NAME.to_string(), true, 0.0, 0.0) + } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq)]