From 337644206ef48eeed40853e1f16447518ae19e99 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 27 Feb 2025 08:44:22 +0000 Subject: [PATCH 1/9] add delegations query support for Cosmos Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 13 ++++- .../coins/rpc_command/tendermint/staking.rs | 27 ++++++++-- mm2src/coins/tendermint/tendermint_coin.rs | 51 ++++++++++++++++++- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index d442960579..b0bee6c359 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2242,6 +2242,7 @@ pub struct DelegationsInfo { #[serde(tag = "type")] pub enum DelegationsInfoDetails { Qtum, + Cosmos(rpc_command::tendermint::staking::DelegationsQuery), } #[derive(Deserialize)] @@ -2253,7 +2254,7 @@ pub struct ValidatorsInfo { #[derive(Debug, Deserialize)] #[serde(tag = "type")] pub enum ValidatorsInfoDetails { - Cosmos(rpc_command::tendermint::staking::ValidatorsRPC), + Cosmos(rpc_command::tendermint::staking::ValidatorsQuery), } #[derive(Serialize, Deserialize)] @@ -4998,6 +4999,16 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result match coin { + MmCoinEnum::Tendermint(t) => Ok(t.delegations_list(r.paging).await.map(|v| json!(v))?), + MmCoinEnum::TendermintToken(t) => Ok(t.platform_coin.delegations_list(r.paging).await.map(|v| json!(v))?), + _ => { + return MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }); + }, + }, } } diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index a9c81f9696..6f4159ec6a 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -30,7 +30,7 @@ impl ToString for ValidatorStatus { } #[derive(Debug, Deserialize)] -pub struct ValidatorsRPC { +pub struct ValidatorsQuery { #[serde(flatten)] paging: PagingOptions, #[serde(default)] @@ -38,7 +38,7 @@ pub struct ValidatorsRPC { } #[derive(Clone, Serialize)] -pub struct ValidatorsRPCResponse { +pub struct ValidatorsQueryResponse { validators: Vec, } @@ -59,8 +59,8 @@ impl From for StakingInfoError { pub async fn validators_rpc( coin: MmCoinEnum, - req: ValidatorsRPC, -) -> Result> { + req: ValidatorsQuery, +) -> Result> { fn maybe_jsonize_description(description: Option) -> Option { description.map(|d| { json!({ @@ -121,7 +121,7 @@ pub async fn validators_rpc( }, }; - Ok(ValidatorsRPCResponse { + Ok(ValidatorsQueryResponse { validators: validators.into_iter().map(jsonize_validator).collect(), }) } @@ -151,3 +151,20 @@ pub struct ClaimRewardsPayload { #[serde(default)] pub force: bool, } + +#[derive(Debug, Deserialize)] +pub struct DelegationsQuery { + #[serde(flatten)] + pub paging: PagingOptions, +} + +#[derive(Serialize)] +pub struct DelegationsQueryResponse { + pub(crate) delegations: Vec, +} + +#[derive(Serialize)] +pub(crate) struct Delegation { + pub(crate) validator_address: String, + pub(crate) delegated_amount: BigDecimal, +} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 7bb12b9186..4256bf9a81 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,7 +6,8 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; -use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, DelegationPayload, ValidatorStatus}; +use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, + DelegationsQueryResponse, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -43,7 +44,9 @@ use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetLatestBlockRequest, GetLatestBlockResponse}; use cosmrs::proto::cosmos::base::v1beta1::{Coin as CoinProto, DecCoin}; use cosmrs::proto::cosmos::distribution::v1beta1::{QueryDelegationRewardsRequest, QueryDelegationRewardsResponse}; -use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, QueryValidatorsRequest, +use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, + QueryDelegatorDelegationsRequest, QueryDelegatorDelegationsResponse, + QueryValidatorsRequest, QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; @@ -96,6 +99,7 @@ const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; const ABCI_VALIDATORS_PATH: &str = "/cosmos.staking.v1beta1.Query/Validators"; const ABCI_DELEGATION_PATH: &str = "/cosmos.staking.v1beta1.Query/Delegation"; +const ABCI_DELEGATOR_DELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorDelegations"; const ABCI_DELEGATION_REWARDS_PATH: &str = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -2626,6 +2630,49 @@ impl TendermintCoin { memo: Some(req.memo), }) } + + pub(crate) async fn delegations_list( + &self, + paging: PagingOptions, + ) -> MmResult { + let request = QueryDelegatorDelegationsRequest { + delegator_addr: self.account_id.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATOR_DELEGATIONS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryDelegatorDelegationsResponse::decode(raw_response.value.as_slice())?; + let delegations = decoded_proto + .delegation_responses + .into_iter() + .filter_map(|r| { + let delegation = r.delegation?; + let amount: u64 = r.balance?.amount.parse().ok()?; + Some(Delegation { + validator_address: delegation.validator_address, + delegated_amount: big_decimal_from_sat_unsigned(amount, self.decimals()), + }) + }) + .collect(); + + Ok(DelegationsQueryResponse { delegations }) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { From 217179a86e8ab7fc5ce7b36687c87c35289463b2 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 27 Feb 2025 09:20:35 +0000 Subject: [PATCH 2/9] add coverage for tendermint delegations_list Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 8 ++-- .../coins/rpc_command/tendermint/staking.rs | 4 +- mm2src/coins/tendermint/tendermint_coin.rs | 46 ++++++++++++++++++- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index b0bee6c359..89aec3bede 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -5003,11 +5003,9 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result match coin { MmCoinEnum::Tendermint(t) => Ok(t.delegations_list(r.paging).await.map(|v| json!(v))?), MmCoinEnum::TendermintToken(t) => Ok(t.platform_coin.delegations_list(r.paging).await.map(|v| json!(v))?), - _ => { - return MmError::err(StakingInfoError::InvalidPayload { - reason: format!("{} is not a Cosmos coin", req.coin), - }); - }, + _ => MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }), }, } } diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 6f4159ec6a..e0d8dc293a 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -158,12 +158,12 @@ pub struct DelegationsQuery { pub paging: PagingOptions, } -#[derive(Serialize)] +#[derive(Debug, PartialEq, Serialize)] pub struct DelegationsQueryResponse { pub(crate) delegations: Vec, } -#[derive(Serialize)] +#[derive(Debug, PartialEq, Serialize)] pub(crate) struct Delegation { pub(crate) validator_address: String, pub(crate) delegated_amount: BigDecimal, diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 4256bf9a81..6f3ed6e3e5 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3795,7 +3795,7 @@ pub mod tendermint_coin_tests { use common::{block_on, wait_until_ms, DEX_FEE_ADDR_RAW_PUBKEY}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventResponse}; use crypto::privkey::key_pair_from_seed; - use std::mem::discriminant; + use std::{mem::discriminant, num::NonZeroUsize}; pub const IRIS_TESTNET_HTLC_PAIR1_SEED: &str = "iris test seed"; // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &[u8] = &[ @@ -4762,4 +4762,48 @@ pub mod tendermint_coin_tests { // tx fee must be taken into account assert!(reward_amount > res.my_balance_change); } + + #[test] + fn test_delegations_list() { + let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; + let protocol_conf = get_iris_protocol(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + nodes, + false, + activation_policy, + false, + )) + .unwrap(); + + let expected_list = DelegationsQueryResponse { + delegations: vec![Delegation { + validator_address: "iva1svannhv2zaxefq83m7treg078udfk37lpjufkw".to_owned(), + delegated_amount: BigDecimal::from_str("1.98").unwrap(), + }], + }; + + let actual_list = block_on(coin.delegations_list(PagingOptions { + limit: 0, + page_number: NonZeroUsize::new(1).unwrap(), + from_uuid: None, + })) + .unwrap(); + + assert_eq!(expected_list, actual_list); + } } From f3db4c36104ca57be96e0204cf73fefaaecdd0de Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 27 Feb 2025 10:37:07 +0000 Subject: [PATCH 3/9] include rewards in delegations and impl undelegations Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 32 +++++- .../coins/rpc_command/tendermint/staking.rs | 21 +++- mm2src/coins/tendermint/tendermint_coin.rs | 107 +++++++++++++++--- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 8 +- 4 files changed, 149 insertions(+), 19 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 89aec3bede..47ec0dfffc 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2242,7 +2242,19 @@ pub struct DelegationsInfo { #[serde(tag = "type")] pub enum DelegationsInfoDetails { Qtum, - Cosmos(rpc_command::tendermint::staking::DelegationsQuery), + Cosmos(rpc_command::tendermint::staking::SimpleListQuery), +} + +#[derive(Deserialize)] +pub struct UndelegationsInfo { + pub coin: String, + info_details: UndelegationsInfoDetails, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum UndelegationsInfoDetails { + Cosmos(rpc_command::tendermint::staking::SimpleListQuery), } #[derive(Deserialize)] @@ -5010,6 +5022,24 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result Result> { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + match req.info_details { + UndelegationsInfoDetails::Cosmos(r) => match coin { + MmCoinEnum::Tendermint(t) => Ok(t.ongoing_undelegations_list(r.paging).await.map(|v| json!(v))?), + MmCoinEnum::TendermintToken(t) => Ok(t + .platform_coin + .ongoing_undelegations_list(r.paging) + .await + .map(|v| json!(v))?), + _ => MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }), + }, + } +} + pub async fn validators_info(ctx: MmArc, req: ValidatorsInfo) -> Result> { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index e0d8dc293a..22f15f84a8 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -153,7 +153,7 @@ pub struct ClaimRewardsPayload { } #[derive(Debug, Deserialize)] -pub struct DelegationsQuery { +pub struct SimpleListQuery { #[serde(flatten)] pub paging: PagingOptions, } @@ -167,4 +167,23 @@ pub struct DelegationsQueryResponse { pub(crate) struct Delegation { pub(crate) validator_address: String, pub(crate) delegated_amount: BigDecimal, + pub(crate) reward_amount: BigDecimal, +} + +#[derive(Serialize)] +pub struct UndelegationsQueryResponse { + pub(crate) ongoing_undelegations: Vec, +} + +#[derive(Serialize)] +pub(crate) struct Undelegation { + pub(crate) validator_address: String, + pub(crate) entries: Vec, +} + +#[derive(Serialize)] +pub(crate) struct UndelegationEntry { + pub(crate) creation_height: i64, + pub(crate) completion_datetime: String, + pub(crate) balance: BigDecimal, } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 6f3ed6e3e5..f0642d397c 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -7,7 +7,8 @@ use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, - DelegationsQueryResponse, ValidatorStatus}; + DelegationsQueryResponse, Undelegation, UndelegationEntry, + UndelegationsQueryResponse, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -46,7 +47,8 @@ use cosmrs::proto::cosmos::base::v1beta1::{Coin as CoinProto, DecCoin}; use cosmrs::proto::cosmos::distribution::v1beta1::{QueryDelegationRewardsRequest, QueryDelegationRewardsResponse}; use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, QueryDelegatorDelegationsRequest, QueryDelegatorDelegationsResponse, - QueryValidatorsRequest, + QueryDelegatorUnbondingDelegationsRequest, + QueryDelegatorUnbondingDelegationsResponse, QueryValidatorsRequest, QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; @@ -100,6 +102,7 @@ const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; const ABCI_VALIDATORS_PATH: &str = "/cosmos.staking.v1beta1.Query/Validators"; const ABCI_DELEGATION_PATH: &str = "/cosmos.staking.v1beta1.Query/Delegation"; const ABCI_DELEGATOR_DELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorDelegations"; +const ABCI_DELEGATOR_UNDELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorUnbondingDelegations"; const ABCI_DELEGATION_REWARDS_PATH: &str = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -2658,20 +2661,88 @@ impl TendermintCoin { .await?; let decoded_proto = QueryDelegatorDelegationsResponse::decode(raw_response.value.as_slice())?; - let delegations = decoded_proto - .delegation_responses + + let mut delegations = Vec::new(); + for response in decoded_proto.delegation_responses { + let Some(delegation) = response.delegation else { continue }; + let Some(balance) = response.balance else { continue }; + + let account_id = AccountId::from_str(&delegation.validator_address) + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + let reward_amount = self + .get_delegation_reward_amount(&account_id) + .await + .map_err(|e| TendermintCoinRpcError::InvalidResponse(e.to_string()))?; + + let amount = balance + .amount + .parse::() + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + delegations.push(Delegation { + validator_address: delegation.validator_address, + delegated_amount: big_decimal_from_sat_unsigned(amount, self.decimals()), + reward_amount, + }); + } + + Ok(DelegationsQueryResponse { delegations }) + } + + pub(crate) async fn ongoing_undelegations_list( + &self, + paging: PagingOptions, + ) -> MmResult { + let request = QueryDelegatorUnbondingDelegationsRequest { + delegator_addr: self.account_id.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATOR_UNDELEGATIONS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryDelegatorUnbondingDelegationsResponse::decode(raw_response.value.as_slice())?; + let ongoing_undelegations = decoded_proto + .unbonding_responses .into_iter() - .filter_map(|r| { - let delegation = r.delegation?; - let amount: u64 = r.balance?.amount.parse().ok()?; - Some(Delegation { - validator_address: delegation.validator_address, - delegated_amount: big_decimal_from_sat_unsigned(amount, self.decimals()), - }) + .map(|r| { + let entries = r + .entries + .into_iter() + .filter_map(|e| { + let balance: u64 = e.balance.parse().ok()?; + + Some(UndelegationEntry { + creation_height: e.creation_height, + completion_datetime: e.completion_time?.to_string(), + balance: big_decimal_from_sat_unsigned(balance, self.decimals()), + }) + }) + .collect(); + + Undelegation { + validator_address: r.validator_address, + entries, + } }) .collect(); - Ok(DelegationsQueryResponse { delegations }) + Ok(UndelegationsQueryResponse { ongoing_undelegations }) } } @@ -4790,19 +4861,27 @@ pub mod tendermint_coin_tests { )) .unwrap(); + let validator_address = "iva1svannhv2zaxefq83m7treg078udfk37lpjufkw"; + let reward_amount = + block_on(coin.get_delegation_reward_amount(&AccountId::from_str(validator_address).unwrap())).unwrap(); + let expected_list = DelegationsQueryResponse { delegations: vec![Delegation { - validator_address: "iva1svannhv2zaxefq83m7treg078udfk37lpjufkw".to_owned(), + validator_address: validator_address.to_owned(), delegated_amount: BigDecimal::from_str("1.98").unwrap(), + reward_amount: reward_amount.round(4), }], }; - let actual_list = block_on(coin.delegations_list(PagingOptions { + let mut actual_list = block_on(coin.delegations_list(PagingOptions { limit: 0, page_number: NonZeroUsize::new(1).unwrap(), from_uuid: None, })) .unwrap(); + for delegation in &mut actual_list.delegations { + delegation.reward_amount = delegation.reward_amount.round(4); + } assert_eq!(expected_list, actual_list); } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index a399d04c35..f3286e5ab5 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -46,8 +46,9 @@ use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::z_coin::ZCoin; use coins::{add_delegation, claim_staking_rewards, delegations_info, get_my_address, get_raw_transaction, - get_swap_transaction_fee_policy, nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, - sign_raw_transaction, validators_info, verify_message, withdraw}; + get_swap_transaction_fee_policy, nft, ongoing_undelegations_info, remove_delegation, + set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, validators_info, verify_message, + withdraw}; use coins_activation::{cancel_init_l2, cancel_init_platform_coin_with_tokens, cancel_init_standalone_coin, cancel_init_token, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, init_l2_user_action, init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, @@ -468,8 +469,9 @@ async fn staking_dispatcher( staking_query_method: &str, ) -> DispatcherResult>> { match staking_query_method { - "validators" => handle_mmrpc(ctx, request, validators_info).await, "delegations" => handle_mmrpc(ctx, request, delegations_info).await, + "ongoing_undelegations" => handle_mmrpc(ctx, request, ongoing_undelegations_info).await, + "validators" => handle_mmrpc(ctx, request, validators_info).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } From 404b78b5d1f1fd4431e9244cba6e4184684e2a14 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 27 Feb 2025 10:39:14 +0000 Subject: [PATCH 4/9] add TODO Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index f0642d397c..688f2cd682 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2690,6 +2690,7 @@ impl TendermintCoin { Ok(DelegationsQueryResponse { delegations }) } + /// TODO: needs coverage pub(crate) async fn ongoing_undelegations_list( &self, paging: PagingOptions, From 45f1a67fab11ddfa205d05cac1ff16f49f9eda50 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 27 Feb 2025 15:00:25 +0300 Subject: [PATCH 5/9] nit Signed-off-by: onur-ozkan --- mm2src/coins/rpc_command/tendermint/staking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 22f15f84a8..190477b7bd 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -155,7 +155,7 @@ pub struct ClaimRewardsPayload { #[derive(Debug, Deserialize)] pub struct SimpleListQuery { #[serde(flatten)] - pub paging: PagingOptions, + pub(crate) paging: PagingOptions, } #[derive(Debug, PartialEq, Serialize)] From 77b8c6345a28e22fcbaae06ac6bd612864e3ebbb Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 3 Mar 2025 07:30:51 +0000 Subject: [PATCH 6/9] extend the coverage on delegations and undelegations Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 1 - .../tests/docker_tests/tendermint_tests.rs | 17 +++-- mm2src/mm2_test_helpers/src/for_tests.rs | 66 +++++++++++++++---- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 688f2cd682..f0642d397c 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2690,7 +2690,6 @@ impl TendermintCoin { Ok(DelegationsQueryResponse { delegations }) } - /// TODO: needs coverage pub(crate) async fn ongoing_undelegations_list( &self, paging: PagingOptions, diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 52788ab1df..de8a215c19 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,7 +5,8 @@ use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_ enable_tendermint_token, enable_tendermint_without_balance, get_tendermint_my_tx_history, ibc_withdraw, iris_ibc_nucleus_testnet_conf, my_balance, nucleus_testnet_conf, orderbook, orderbook_v2, send_raw_transaction, - set_price, tendermint_add_delegation, tendermint_remove_delegation, + set_price, tendermint_add_delegation, tendermint_delegations, + tendermint_ongoing_undelegations, tendermint_remove_delegation, tendermint_remove_delegation_raw, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, @@ -760,6 +761,10 @@ fn test_tendermint_remove_delegation() { let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex)); log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); + let r = block_on(tendermint_delegations(&mm, coin_ticker)); + let delegation_info = r["result"]["delegations"].as_array().unwrap().last().unwrap(); + assert_eq!(delegation_info["validator_address"], VALIDATOR_ADDRESS); + // Try to undelegate more than the total delegated amount let raw_response = block_on(tendermint_remove_delegation_raw( &mm, @@ -777,17 +782,17 @@ fn test_tendermint_remove_delegation() { }; assert!(raw_response.1.contains("TooMuchToUndelegate")); - // TODO: check currently delegated stakes and assert them - // This requires delegation listing feature - let tx_details = block_on(tendermint_remove_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5")); assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); assert!(tx_details.to.is_empty()); assert_eq!(tx_details.transaction_type, TransactionType::RemoveDelegation); - // TODO: check currently delegated stakes and assert them - // This requires delegation listing feature + let r = block_on(tendermint_ongoing_undelegations(&mm, coin_ticker)); + let undelegation_info = r["result"]["ongoing_undelegations"].as_array().unwrap().last().unwrap(); + assert_eq!(undelegation_info["validator_address"], VALIDATOR_ADDRESS); + let undelegation_entry = undelegation_info["entries"].as_array().unwrap().last().unwrap(); + assert_eq!(undelegation_entry["balance"], "0.5"); } mod swap { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 9d51dbdfeb..fae56dc835 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -237,10 +237,7 @@ pub const QRC20_ELECTRUMS: &[&str] = &[ "electrum3.cipig.net:10071", ]; pub const T_BCH_ELECTRUMS: &[&str] = &["tbch.loping.net:60001", "bch0.kister.net:51001"]; -pub const TBTC_ELECTRUMS: &[&str] = &[ - "electrum3.cipig.net:10068", - "testnet.aranguren.org:51001", -]; +pub const TBTC_ELECTRUMS: &[&str] = &["electrum3.cipig.net:10068", "testnet.aranguren.org:51001"]; pub const ETH_MAINNET_NODES: &[&str] = &[ "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b", @@ -831,13 +828,9 @@ pub fn eth_testnet_conf_trezor() -> Json { } /// ETH configuration used for dockerized Geth dev node -pub fn eth_dev_conf() -> Json { - eth_conf("ETH") -} +pub fn eth_dev_conf() -> Json { eth_conf("ETH") } -pub fn eth1_dev_conf() -> Json { - eth_conf("ETH1") -} +pub fn eth1_dev_conf() -> Json { eth_conf("ETH1") } fn eth_conf(coin: &str) -> Json { json!({ @@ -2102,7 +2095,12 @@ pub async fn enable_eth_coin_v2( })) .await .unwrap(); - assert_eq!(enable.0, StatusCode::OK, "'enable_eth_with_tokens' failed: {}", enable.1); + assert_eq!( + enable.0, + StatusCode::OK, + "'enable_eth_with_tokens' failed: {}", + enable.1 + ); json::from_str(&enable.1).unwrap() } @@ -3133,6 +3131,52 @@ pub async fn get_tendermint_my_tx_history(mm: &MarketMakerIt, coin: &str, limit: json::from_str(&request.1).unwrap() } +pub async fn tendermint_delegations(mm: &MarketMakerIt, coin: &str) -> Json { + let rpc_endpoint = "experimental::staking::query::delegations"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "info_details": { + "type": "Cosmos", + "limit": 0, + "page_number": 1 + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'{rpc_endpoint}' failed: {}", request.1); + log!("{rpc_endpoint} response {}", request.1); + json::from_str(&request.1).unwrap() +} + +pub async fn tendermint_ongoing_undelegations(mm: &MarketMakerIt, coin: &str) -> Json { + let rpc_endpoint = "experimental::staking::query::ongoing_undelegations"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "info_details": { + "type": "Cosmos", + "limit": 0, + "page_number": 1 + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'{rpc_endpoint}' failed: {}", request.1); + log!("{rpc_endpoint} response {}", request.1); + json::from_str(&request.1).unwrap() +} + pub async fn enable_tendermint_token(mm: &MarketMakerIt, coin: &str) -> Json { let request = json!({ "userpass": mm.userpass, From 7bcf53389024c92b7df9ce77b351fef6751806ec Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 3 Mar 2025 08:09:21 +0000 Subject: [PATCH 7/9] handle empty reward case on delegations list Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 11 ++++++---- .../tests/docker_tests/tendermint_tests.rs | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index f0642d397c..79ba11d9eb 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2670,10 +2670,13 @@ impl TendermintCoin { let account_id = AccountId::from_str(&delegation.validator_address) .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; - let reward_amount = self - .get_delegation_reward_amount(&account_id) - .await - .map_err(|e| TendermintCoinRpcError::InvalidResponse(e.to_string()))?; + let reward_amount = match self.get_delegation_reward_amount(&account_id).await { + Ok(reward) => reward, + Err(e) => match e.into_inner() { + DelegationError::NothingToClaim { .. } => BigDecimal::zero(), + e => return MmError::err(TendermintCoinRpcError::InvalidResponse(e.to_string())), + }, + }; let amount = balance .amount diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index de8a215c19..00e2399556 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -1,4 +1,5 @@ use common::{block_on, log}; +use instant::Duration; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_err, enable_tendermint, @@ -15,6 +16,7 @@ use serde_json::json; use std::collections::HashSet; use std::iter::FromIterator; use std::sync::Mutex; +use std::thread; const TENDERMINT_TEST_SEED: &str = "tendermint test seed"; const TENDERMINT_CONSTANT_BALANCE_SEED: &str = "tendermint constant balance seed"; @@ -761,6 +763,8 @@ fn test_tendermint_remove_delegation() { let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex)); log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); + thread::sleep(Duration::from_secs(1)); + let r = block_on(tendermint_delegations(&mm, coin_ticker)); let delegation_info = r["result"]["delegations"].as_array().unwrap().last().unwrap(); assert_eq!(delegation_info["validator_address"], VALIDATOR_ADDRESS); @@ -782,17 +786,27 @@ fn test_tendermint_remove_delegation() { }; assert!(raw_response.1.contains("TooMuchToUndelegate")); - let tx_details = block_on(tendermint_remove_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5")); + let tx_details = block_on(tendermint_remove_delegation( + &mm, + coin_ticker, + VALIDATOR_ADDRESS, + "0.15", + )); assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); assert!(tx_details.to.is_empty()); assert_eq!(tx_details.transaction_type, TransactionType::RemoveDelegation); + let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex)); + log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); + + thread::sleep(Duration::from_secs(1)); + let r = block_on(tendermint_ongoing_undelegations(&mm, coin_ticker)); let undelegation_info = r["result"]["ongoing_undelegations"].as_array().unwrap().last().unwrap(); assert_eq!(undelegation_info["validator_address"], VALIDATOR_ADDRESS); let undelegation_entry = undelegation_info["entries"].as_array().unwrap().last().unwrap(); - assert_eq!(undelegation_entry["balance"], "0.5"); + assert_eq!(undelegation_entry["balance"], "0.15"); } mod swap { @@ -804,14 +818,13 @@ mod swap { use common::executor::Timer; use common::log; use ethereum_types::{Address, U256}; - use instant::Duration; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, doc_conf, enable_eth_coin, iris_ibc_nucleus_testnet_conf, nucleus_testnet_conf, wait_check_stats_swap_status, DOC_ELECTRUM_ADDRS}; use std::convert::TryFrom; + use std::env; use std::str::FromStr; - use std::{env, thread}; const BOB_PASSPHRASE: &str = "iris test seed"; const ALICE_PASSPHRASE: &str = "iris test2 seed"; From d1115e866f2456f92284fc8964f88ee7a8b92324 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 10 Mar 2025 07:10:32 +0000 Subject: [PATCH 8/9] keep the error with `MmError` wrapper Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 79ba11d9eb..be93b5fab3 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2672,9 +2672,9 @@ impl TendermintCoin { let reward_amount = match self.get_delegation_reward_amount(&account_id).await { Ok(reward) => reward, - Err(e) => match e.into_inner() { + Err(e) => match e.get_inner() { DelegationError::NothingToClaim { .. } => BigDecimal::zero(), - e => return MmError::err(TendermintCoinRpcError::InvalidResponse(e.to_string())), + _ => return MmError::err(TendermintCoinRpcError::InvalidResponse(e.to_string())), }, }; From 70b217f4eb50453a5a988f216bd3f11e727b5d93 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 19 Mar 2025 06:04:11 +0000 Subject: [PATCH 9/9] handle tokens Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 47ec0dfffc..4451001843 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -5014,7 +5014,9 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result match coin { MmCoinEnum::Tendermint(t) => Ok(t.delegations_list(r.paging).await.map(|v| json!(v))?), - MmCoinEnum::TendermintToken(t) => Ok(t.platform_coin.delegations_list(r.paging).await.map(|v| json!(v))?), + MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + reason: "Tokens are not supported for delegation".into(), + }), _ => MmError::err(StakingInfoError::InvalidPayload { reason: format!("{} is not a Cosmos coin", req.coin), }), @@ -5028,11 +5030,9 @@ pub async fn ongoing_undelegations_info(ctx: MmArc, req: UndelegationsInfo) -> R match req.info_details { UndelegationsInfoDetails::Cosmos(r) => match coin { MmCoinEnum::Tendermint(t) => Ok(t.ongoing_undelegations_list(r.paging).await.map(|v| json!(v))?), - MmCoinEnum::TendermintToken(t) => Ok(t - .platform_coin - .ongoing_undelegations_list(r.paging) - .await - .map(|v| json!(v))?), + MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + reason: "Tokens are not supported for delegation".into(), + }), _ => MmError::err(StakingInfoError::InvalidPayload { reason: format!("{} is not a Cosmos coin", req.coin), }),