Skip to content

Commit

Permalink
feat(tendermint): validators RPC (#2310)
Browse files Browse the repository at this point in the history
* save dev state

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* save dev state

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* make proto types serializable for RPC endpoint

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* remove dummy test

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* add RPC error type

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* add TODO

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* fix status filtering

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* fix clippy warn

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* resolve `todo!()`s

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* remove inline attribute

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* improve `validators_rpc`

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* add coverage for tendermint_validators RPC

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* apply nit changes

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* document `ValidatorStatus`

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* use proper error variant on coin filtering

Signed-off-by: onur-ozkan <work@onurozkan.dev>

* apply nits

Signed-off-by: onur-ozkan <work@onurozkan.dev>

---------

Signed-off-by: onur-ozkan <work@onurozkan.dev>
  • Loading branch information
onur-ozkan authored Jan 8, 2025
1 parent a9402c6 commit 1908a2e
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 23 deletions.
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
150 changes: 150 additions & 0 deletions mm2src/coins/rpc_command/tendermint/staking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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 still possible to delegate tokens to a validator in this state.
Unbonded,
}

impl ToString for ValidatorStatus {
fn to_string(&self) -> String {
match self {
// An empty string doesn't filter any validators and we get an unfiltered result.
ValidatorStatus::All => String::default(),
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,
#[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 @@ -454,8 +462,9 @@ impl From<TendermintCoinRpcError> for BalanceError {
match err {
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::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => {
BalanceError::Transport(e)
},
TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e),
TendermintCoinRpcError::UnexpectedAccountType { prefix } => {
BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs"))
Expand All @@ -469,8 +478,9 @@ impl From<TendermintCoinRpcError> for ValidatePaymentError {
match err {
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::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => {
ValidatePaymentError::Transport(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"
);
assert_eq!(validators_raw_response["result"]["validators"][0]["jailed"], false);
}

mod swap {
use super::*;

Expand Down
Loading

0 comments on commit 1908a2e

Please sign in to comment.