Skip to content
41 changes: 40 additions & 1 deletion mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2238,6 +2238,19 @@ pub struct DelegationsInfo {
#[serde(tag = "type")]
pub enum DelegationsInfoDetails {
Qtum,
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),
Comment on lines +2241 to +2253
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is so much code in lp_coins.rs related to staking and delegation.

What about moving it to pub mod staking? The module can be inside the coins workspace.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... I also hate that, but the thing is even with staking module in coins module it wouldn't be good enough, in fact, I think it will be even bad because it will look like we have duplicated staking modules (one from tendermint and one from coins) and it will look inconsistent with how we handle other RPCs (right now they all included in lp_coins).

I would prefer to re-write this module in general, rather than applying a workaround solution for specific logic.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be even bad because it will look like we have duplicated staking modules

You can rename staking in tendermint or just add doc comment at the top of file

Copy link
Copy Markdown
Author

@onur-ozkan onur-ozkan Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do it in this PR, it will be very hard for others to review this PR (due to moving lots of things around). I will wait for others opinion on this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can create an issue to move staking code from lp_coins to its own module before adding more staking implementations.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do it in this PR, it will be very hard for others to review this PR (due to moving lots of things around). I will wait for others opinion on this.

Agreed, we should never do refactors that involves moving code around in the same PR as new functionalities.

}

#[derive(Deserialize)]
Expand All @@ -2249,7 +2262,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)]
Expand Down Expand Up @@ -4994,6 +5007,32 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result<Json,

qtum.get_delegation_infos().compat().await.map(|v| json!(v))
},

DelegationsInfoDetails::Cosmos(r) => match coin {
MmCoinEnum::Tendermint(t) => Ok(t.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),
}),
},
}
}

pub async fn ongoing_undelegations_info(ctx: MmArc, req: UndelegationsInfo) -> Result<Json, MmError<StakingInfoError>> {
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(_) => 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),
}),
},
}
}

Expand Down
46 changes: 41 additions & 5 deletions mm2src/coins/rpc_command/tendermint/staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ impl ToString for ValidatorStatus {
}

#[derive(Debug, Deserialize)]
pub struct ValidatorsRPC {
pub struct ValidatorsQuery {
#[serde(flatten)]
paging: PagingOptions,
#[serde(default)]
filter_by_status: ValidatorStatus,
}

#[derive(Clone, Serialize)]
pub struct ValidatorsRPCResponse {
pub struct ValidatorsQueryResponse {
validators: Vec<serde_json::Value>,
}

Expand All @@ -59,8 +59,8 @@ impl From<TendermintCoinRpcError> for StakingInfoError {

pub async fn validators_rpc(
coin: MmCoinEnum,
req: ValidatorsRPC,
) -> Result<ValidatorsRPCResponse, MmError<StakingInfoError>> {
req: ValidatorsQuery,
) -> Result<ValidatorsQueryResponse, MmError<StakingInfoError>> {
fn maybe_jsonize_description(description: Option<Description>) -> Option<serde_json::Value> {
description.map(|d| {
json!({
Expand Down Expand Up @@ -121,7 +121,7 @@ pub async fn validators_rpc(
},
};

Ok(ValidatorsRPCResponse {
Ok(ValidatorsQueryResponse {
validators: validators.into_iter().map(jsonize_validator).collect(),
})
}
Expand Down Expand Up @@ -151,3 +151,39 @@ pub struct ClaimRewardsPayload {
#[serde(default)]
pub force: bool,
}

#[derive(Debug, Deserialize)]
pub struct SimpleListQuery {
#[serde(flatten)]
pub(crate) paging: PagingOptions,
}

#[derive(Debug, PartialEq, Serialize)]
pub struct DelegationsQueryResponse {
pub(crate) delegations: Vec<Delegation>,
}

#[derive(Debug, PartialEq, Serialize)]
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<Undelegation>,
}

#[derive(Serialize)]
pub(crate) struct Undelegation {
pub(crate) validator_address: String,
pub(crate) entries: Vec<UndelegationEntry>,
}

#[derive(Serialize)]
pub(crate) struct UndelegationEntry {
pub(crate) creation_height: i64,
pub(crate) completion_datetime: String,
pub(crate) balance: BigDecimal,
}
179 changes: 176 additions & 3 deletions mm2src/coins/tendermint/tendermint_coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ 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, Undelegation, UndelegationEntry,
UndelegationsQueryResponse, ValidatorStatus};
use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError,
IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError,
IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH,
Expand Down Expand Up @@ -43,7 +45,10 @@ 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,
QueryDelegatorUnbondingDelegationsRequest,
QueryDelegatorUnbondingDelegationsResponse, QueryValidatorsRequest,
QueryValidatorsResponse as QueryValidatorsResponseProto};
use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, SimulateRequest, SimulateResponse, Tx, TxBody,
TxRaw};
Expand Down Expand Up @@ -95,6 +100,8 @@ const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance";
const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx";
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;
Expand Down Expand Up @@ -2617,6 +2624,120 @@ impl TendermintCoin {
memo: Some(req.memo),
})
}

pub(crate) async fn delegations_list(
&self,
paging: PagingOptions,
) -> MmResult<DelegationsQueryResponse, TendermintCoinRpcError> {
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 mut delegations = Vec::new();
Comment thread
borngraced marked this conversation as resolved.
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 = match self.get_delegation_reward_amount(&account_id).await {
Ok(reward) => reward,
Err(e) => match e.get_inner() {
DelegationError::NothingToClaim { .. } => BigDecimal::zero(),
_ => return MmError::err(TendermintCoinRpcError::InvalidResponse(e.to_string())),
},
};

let amount = balance
.amount
.parse::<u64>()
.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<UndelegationsQueryResponse, TendermintCoinRpcError> {
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()
.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()),
})
Comment thread
laruh marked this conversation as resolved.
})
.collect();

Undelegation {
validator_address: r.validator_address,
entries,
}
})
.collect();

Ok(UndelegationsQueryResponse { ongoing_undelegations })
}
}

fn clients_from_urls(ctx: &MmArc, nodes: Vec<RpcNode>) -> MmResult<Vec<HttpClient>, TendermintInitErrorKind> {
Expand Down Expand Up @@ -3729,7 +3850,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};
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] = &[
Expand Down Expand Up @@ -4687,4 +4808,56 @@ 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 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: validator_address.to_owned(),
delegated_amount: BigDecimal::from_str("1.98").unwrap(),
reward_amount: reward_amount.round(4),
}],
};

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);
}
}
8 changes: 5 additions & 3 deletions mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -468,8 +469,9 @@ async fn staking_dispatcher(
staking_query_method: &str,
) -> DispatcherResult<Response<Vec<u8>>> {
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),
}
}
Expand Down
Loading
Loading