Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tendermint): validators RPC #2310

Merged
merged 16 commits into from
Jan 8, 2025
1 change: 1 addition & 0 deletions mm2src/coins/rpc_command/tendermint/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod ibc_chains;
mod ibc_transfer_channels;
pub mod staking;

pub use ibc_chains::*;
pub use ibc_transfer_channels::*;
Expand Down
149 changes: 149 additions & 0 deletions mm2src/coins/rpc_command/tendermint/staking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use common::{HttpStatusCode, PagingOptions, StatusCode};
use cosmrs::staking::{Commission, Description, Validator};
use mm2_core::mm_ctx::MmArc;
use mm2_err_handle::prelude::MmError;

use crate::{lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum};

/// Represents current status of the validator.
#[derive(Default, Deserialize)]
pub(crate) enum ValidatorStatus {
All,
/// Validator is in the active set and participates in consensus.
#[default]
Bonded,
/// Validator is not in the active set and does not participate in consensus.
/// Accordingly, they do not receive rewards and cannot be slashed.
/// It is possible to delegate tokens to a validator in this state.
mariocynicys marked this conversation as resolved.
Show resolved Hide resolved
Unbonded,
}

impl ToString for ValidatorStatus {
fn to_string(&self) -> String {
match self {
ValidatorStatus::All => String::default(),
mariocynicys marked this conversation as resolved.
Show resolved Hide resolved
ValidatorStatus::Bonded => "BOND_STATUS_BONDED".into(),
ValidatorStatus::Unbonded => "BOND_STATUS_UNBONDED".into(),
}
}
}

#[derive(Deserialize)]
pub struct ValidatorsRPC {
#[serde(rename = "ticker")]
coin: String,
#[serde(flatten)]
paging: PagingOptions,
mariocynicys marked this conversation as resolved.
Show resolved Hide resolved
#[serde(default)]
filter_by_status: ValidatorStatus,
}

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

#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)]
#[serde(tag = "error_type", content = "error_data")]
pub enum ValidatorsRPCError {
#[display(fmt = "Coin '{ticker}' could not be found in coins configuration.")]
CoinNotFound { ticker: String },
#[display(fmt = "'{ticker}' is not a Cosmos coin.")]
UnexpectedCoinType { ticker: String },
#[display(fmt = "Transport error: {}", _0)]
Transport(String),
#[display(fmt = "Internal error: {}", _0)]
InternalError(String),
}

impl HttpStatusCode for ValidatorsRPCError {
fn status_code(&self) -> common::StatusCode {
match self {
ValidatorsRPCError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE,
ValidatorsRPCError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ValidatorsRPCError::CoinNotFound { .. } => StatusCode::NOT_FOUND,
ValidatorsRPCError::UnexpectedCoinType { .. } => StatusCode::BAD_REQUEST,
}
}
}

impl From<TendermintCoinRpcError> for ValidatorsRPCError {
fn from(e: TendermintCoinRpcError) -> Self {
match e {
TendermintCoinRpcError::InvalidResponse(e)
| TendermintCoinRpcError::PerformError(e)
| TendermintCoinRpcError::RpcClientError(e) => ValidatorsRPCError::Transport(e),
TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => ValidatorsRPCError::InternalError(e),
TendermintCoinRpcError::UnexpectedAccountType { .. } => ValidatorsRPCError::InternalError(
"RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal."
.into(),
),
}
}
}

pub async fn validators_rpc(
ctx: MmArc,
req: ValidatorsRPC,
) -> Result<ValidatorsRPCResponse, MmError<ValidatorsRPCError>> {
fn maybe_jsonize_description(description: Option<Description>) -> Option<serde_json::Value> {
description.map(|d| {
json!({
"moniker": d.moniker,
"identity": d.identity,
"website": d.website,
"security_contact": d.security_contact,
"details": d.details,
})
})
}

fn maybe_jsonize_commission(commission: Option<Commission>) -> Option<serde_json::Value> {
commission.map(|c| {
let rates = c.commission_rates.map(|cr| {
json!({
"rate": cr.rate,
"max_rate": cr.max_rate,
"max_change_rate": cr.max_change_rate
})
});

json!({
"commission_rates": rates,
"update_time": c.update_time
})
})
}

fn jsonize_validator(v: Validator) -> serde_json::Value {
json!({
"operator_address": v.operator_address,
"consensus_pubkey": v.consensus_pubkey,
"jailed": v.jailed,
"status": v.status,
"tokens": v.tokens,
"delegator_shares": v.delegator_shares,
"description": maybe_jsonize_description(v.description),
"unbonding_height": v.unbonding_height,
"unbonding_time": v.unbonding_time,
"commission": maybe_jsonize_commission(v.commission),
"min_self_delegation": v.min_self_delegation,
})
}

let validators = match lp_coinfind_or_err(&ctx, &req.coin).await {
Ok(MmCoinEnum::Tendermint(coin)) => coin.validators_list(req.filter_by_status, req.paging).await?,
Ok(MmCoinEnum::TendermintToken(token)) => {
token
.platform_coin
.validators_list(req.filter_by_status, req.paging)
.await?
},
Ok(_) => return MmError::err(ValidatorsRPCError::UnexpectedCoinType { ticker: req.coin }),
Err(_) => return MmError::err(ValidatorsRPCError::CoinNotFound { ticker: req.coin }),
};

Ok(ValidatorsRPCResponse {
validators: validators.into_iter().map(jsonize_validator).collect(),
})
}
54 changes: 49 additions & 5 deletions mm2src/coins/tendermint/tendermint_coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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::ValidatorStatus;
use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError,
IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError,
IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH,
Expand Down Expand Up @@ -35,17 +36,21 @@ use bitcrypto::{dhash160, sha256};
use common::executor::{abortable_queue::AbortableQueue, AbortableSystem};
use common::executor::{AbortedError, Timer};
use common::log::{debug, warn};
use common::{get_utc_timestamp, now_sec, Future01CompatExt, DEX_FEE_ADDR_PUBKEY};
use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY};
use cosmrs::bank::MsgSend;
use cosmrs::crypto::secp256k1::SigningKey;
use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse};
use cosmrs::proto::cosmos::bank::v1beta1::{MsgSend as MsgSendProto, QueryBalanceRequest, QueryBalanceResponse};
use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest;
use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse,
GetLatestBlockRequest, GetLatestBlockResponse};
use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto;
use cosmrs::proto::cosmos::staking::v1beta1::{QueryValidatorsRequest,
QueryValidatorsResponse as QueryValidatorsResponseProto};
use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse,
SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw};
use cosmrs::proto::prost::{DecodeError, Message};
use cosmrs::staking::{QueryValidatorsResponse, Validator};
use cosmrs::tendermint::block::Height;
use cosmrs::tendermint::chain::Id as ChainId;
use cosmrs::tendermint::PublicKey;
Expand Down Expand Up @@ -89,6 +94,7 @@ const ABCI_QUERY_ACCOUNT_PATH: &str = "/cosmos.auth.v1beta1.Query/Account";
const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance";
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";

pub(crate) const MIN_TX_SATOSHIS: i64 = 1;

Expand Down Expand Up @@ -423,6 +429,8 @@ pub enum TendermintInitErrorKind {
CantUseWatchersWithPubkeyPolicy,
}

/// TODO: Rename this into `ClientRpcError` because this is very
/// confusing atm.
#[derive(Display, Debug, Serialize, SerializeErrorType)]
#[serde(tag = "error_type", content = "error_data")]
pub enum TendermintCoinRpcError {
Expand Down Expand Up @@ -455,8 +463,9 @@ impl From<TendermintCoinRpcError> for BalanceError {
TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e),
TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e),
TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e),
TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e),
TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e),
TendermintCoinRpcError::RpcClientError(e) | TendermintCoinRpcError::InternalError(e) => {
BalanceError::Internal(e)
},
mariocynicys marked this conversation as resolved.
Show resolved Hide resolved
TendermintCoinRpcError::UnexpectedAccountType { prefix } => {
BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs"))
},
Expand All @@ -470,8 +479,9 @@ impl From<TendermintCoinRpcError> for ValidatePaymentError {
TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e),
TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e),
TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e),
TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e),
TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e),
TendermintCoinRpcError::RpcClientError(e) | TendermintCoinRpcError::InternalError(e) => {
ValidatePaymentError::InternalError(e)
},
TendermintCoinRpcError::UnexpectedAccountType { prefix } => {
ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs"))
},
Expand Down Expand Up @@ -2080,6 +2090,40 @@ impl TendermintCoin {

None
}

pub(crate) async fn validators_list(
&self,
filter_status: ValidatorStatus,
paging: PagingOptions,
) -> MmResult<Vec<Validator>, TendermintCoinRpcError> {
let request = QueryValidatorsRequest {
status: filter_status.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_VALIDATORS_PATH.to_owned()),
request.encode_to_vec(),
ABCI_REQUEST_HEIGHT,
ABCI_REQUEST_PROVE,
)
.await?;

let decoded_proto = QueryValidatorsResponseProto::decode(raw_response.value.as_slice())?;
let typed_response = QueryValidatorsResponse::try_from(decoded_proto)
.map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?;

Ok(typed_response.validators)
}
}

fn clients_from_urls(ctx: &MmArc, nodes: Vec<RpcNode>) -> MmResult<Vec<HttpClient>, TendermintInitErrorKind> {
Expand Down
2 changes: 2 additions & 0 deletions mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::rpc::lp_commands::trezor::trezor_connection_status;
use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext};
use coins::eth::EthCoin;
use coins::my_tx_history_v2::my_tx_history_v2_rpc;
use coins::rpc_command::tendermint::staking::validators_rpc;
use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels};
use coins::rpc_command::{account_balance::account_balance,
get_current_mtp::get_current_mtp_rpc,
Expand Down Expand Up @@ -212,6 +213,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult<Re
"start_version_stat_collection" => handle_mmrpc(ctx, request, start_version_stat_collection).await,
"stop_simple_market_maker_bot" => handle_mmrpc(ctx, request, stop_simple_market_maker_bot).await,
"stop_version_stat_collection" => handle_mmrpc(ctx, request, stop_version_stat_collection).await,
"tendermint_validators" => handle_mmrpc(ctx, request, validators_rpc).await,
"trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await,
"trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await,
"update_nft" => handle_mmrpc(ctx, request, update_nft).await,
Expand Down
28 changes: 27 additions & 1 deletion mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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, withdraw_v1, MarketMakerIt, Mm2TestConf};
set_price, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf};
use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response,
TendermintActivationResult, TransactionDetails};
use serde_json::json;
Expand Down Expand Up @@ -651,6 +651,32 @@ fn test_passive_coin_and_force_disable() {
block_on(disable_coin_err(&mm, token, false));
}

#[test]
fn test_tendermint_validators_rpc() {
let coins = json!([nucleus_testnet_conf()]);
let platform_coin = coins[0]["coin"].as_str().unwrap();

let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins);
let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap();

let activation_res = block_on(enable_tendermint(
&mm,
platform_coin,
&[],
NUCLEUS_TESTNET_RPC_URLS,
false,
));
assert!(&activation_res.get("result").unwrap().get("address").is_some());

let validators_raw_response = block_on(tendermint_validators(&mm, platform_coin, "All", 10, 1));

assert_eq!(
validators_raw_response["result"]["validators"][0]["operator_address"],
"nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu"
);
mariocynicys marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(validators_raw_response["result"]["validators"][0]["jailed"], false);
}

mod swap {
use super::*;

Expand Down
Loading
Loading