use std::collections::{BTreeMap, BTreeSet};
use std::io;
use color_eyre::owo_colors::OwoColorize;
use data_encoding::HEXLOWER;
use either::Either;
use futures::StreamExt;
use masp_primitives::asset_type::AssetType;
use masp_primitives::merkle_tree::MerklePath;
use masp_primitives::sapling::Node;
use masp_primitives::transaction::components::I128Sum;
use masp_primitives::zip32::ExtendedFullViewingKey;
use namada_core::masp::{BalanceOwner, MaspEpoch};
use namada_core::token::Amount;
use namada_sdk::address::{Address, InternalAddress, MASP};
use namada_sdk::borsh::BorshDeserialize;
use namada_sdk::chain::{BlockHeight, Epoch};
use namada_sdk::collections::{HashMap, HashSet};
use namada_sdk::control_flow::time::{Duration, Instant};
use namada_sdk::dec::Dec;
use namada_sdk::governance::ProposalVote;
use namada_sdk::governance::parameters::GovernanceParameters;
use namada_sdk::governance::pgf::parameters::PgfParameters;
use namada_sdk::governance::pgf::storage::steward::StewardDetail;
use namada_sdk::governance::storage::keys as governance_storage;
use namada_sdk::governance::storage::proposal::{
StoragePgfFunding, StorageProposal,
};
use namada_sdk::governance::utils::{ProposalVotes, VotePower};
use namada_sdk::hash::Hash;
use namada_sdk::ibc::parameters::{IbcParameters, IbcTokenRateLimits};
use namada_sdk::io::{Client, Io, display, display_line, edisplay_line};
use namada_sdk::key::*;
use namada_sdk::masp::MaspTokenRewardData;
use namada_sdk::masp::shielded_wallet::ShieldedApi;
use namada_sdk::parameters::{
EpochDuration, ProposalBytes, storage as param_storage,
};
use namada_sdk::proof_of_stake::rewards::PosRewardsRates;
use namada_sdk::proof_of_stake::types::{
CommissionPair, Slash, ValidatorMetaData, ValidatorState,
ValidatorStateInfo, WeightedValidator,
};
use namada_sdk::proof_of_stake::{self, OwnedPosParams, PosParams};
use namada_sdk::queries::RPC;
use namada_sdk::rpc::{
self, TxAppliedEvents, TxResponse, enriched_bonds_and_unbonds,
format_denominated_amount, get_effective_native_supply, query_epoch,
query_ibc_params,
};
use namada_sdk::state::LastBlock;
use namada_sdk::storage::BlockResults;
use namada_sdk::tendermint_rpc::endpoint::status;
use namada_sdk::time::DateTimeUtc;
use namada_sdk::token::{DenominatedAmount, MaspDigitPos};
use namada_sdk::tx::display_batch_resp;
use namada_sdk::wallet::AddressVpType;
use namada_sdk::{Namada, error, state as storage, token};
use crate::cli::{self, args};
use crate::tendermint::merkle::proof::ProofOps;
pub async fn query_tx_status(
namada: &impl Namada,
status: namada_sdk::rpc::TxEventQuery<'_>,
deadline: Instant,
) -> TxAppliedEvents {
rpc::query_tx_status(namada, status, deadline)
.await
.unwrap()
}
pub async fn query_and_print_epoch(context: &impl Namada) -> Epoch {
let epoch = rpc::query_epoch(context.client()).await.unwrap();
display_line!(context.io(), "Last committed epoch: {}", epoch);
epoch
}
pub async fn query_and_print_masp_epoch(context: &impl Namada) -> MaspEpoch {
let epoch = rpc::query_masp_epoch(context.client()).await.unwrap();
display_line!(context.io(), "Last committed masp epoch: {}", epoch);
epoch
}
pub async fn query_and_print_next_epoch_info(context: &impl Namada) {
println!();
let current_block = query_block(context).await.unwrap();
let current_epoch = query_epoch(context.client()).await.unwrap();
let (this_epoch_first_height, epoch_duration) =
rpc::query_next_epoch_info(context.client()).await.unwrap();
let this_epoch_first_height_header =
rpc::query_block_header(context.client(), this_epoch_first_height)
.await
.unwrap()
.unwrap();
let first_block_time = this_epoch_first_height_header.time;
let next_epoch_time = first_block_time + epoch_duration.min_duration;
#[allow(clippy::disallowed_methods)]
let current_time = DateTimeUtc::now();
let seconds_left = next_epoch_time.time_diff(current_time).0;
let blocks_left =
(this_epoch_first_height.0 + epoch_duration.min_num_of_blocks + 2u64)
.saturating_sub(current_block.height.0);
let time_remaining_str = convert_to_hours(seconds_left);
display_line!(context.io(), "\nCurrent epoch: {current_epoch}.");
display_line!(
context.io(),
"First block height of epoch {current_epoch}: \
{this_epoch_first_height}.\n"
);
display_line!(
context.io(),
"Minimum number of blocks in an epoch: {}.",
epoch_duration.min_num_of_blocks
);
display_line!(
context.io(),
"Minimum amount of time for an epoch: {}.",
convert_to_hours(epoch_duration.min_duration.0)
);
if seconds_left == 0 {
display_line!(
context.io(),
"\nNext epoch ({}) begins in {} blocks\n",
current_epoch.next(),
blocks_left
);
} else if blocks_left == 0 {
display_line!(
context.io(),
"\nNext epoch ({}) begins in {}\n",
current_epoch.next(),
time_remaining_str
);
} else {
display_line!(
context.io(),
"\nNext epoch ({}) begins in {} or in {} blocks, whichever occurs \
later.\n",
current_epoch.next(),
time_remaining_str,
blocks_left
);
}
}
fn convert_to_hours(seconds: u64) -> String {
let hours = seconds / 3600;
let minutes = (seconds - 3600 * hours) / 60;
let seconds_unit = seconds - 3600 * hours - 60 * minutes;
if hours > 0 {
format!("{}h-{}m-{}s", hours, minutes, seconds_unit)
} else if minutes > 0 {
format!("{}m-{}s", minutes, seconds_unit)
} else {
format!("{}s", seconds_unit)
}
}
pub async fn query_and_print_status(
context: &impl Namada,
) -> Option<status::Response> {
let status = context.client().status().await;
match status {
Ok(status) => {
display_line!(context.io(), "Node's status {status:#?}");
Some(status)
}
Err(err) => {
edisplay_line!(context.io(), "Status query failed with {err:#?}");
None
}
}
}
pub async fn query_block(context: &impl Namada) -> Option<LastBlock> {
let block = namada_sdk::rpc::query_block(context.client())
.await
.unwrap();
match block {
Some(block) => {
display_line!(
context.io(),
"Last committed block height: {}, time: {}",
block.height,
block.time
);
Some(block)
}
None => {
display_line!(context.io(), "No block has been committed yet.");
None
}
}
}
pub async fn query_results<C: Client + Sync>(
client: &C,
_args: args::Query,
) -> Vec<BlockResults> {
unwrap_client_response::<C, Vec<BlockResults>>(
RPC.shell().read_results(client).await,
)
}
pub async fn query_raw_bytes<N: Namada>(
context: &N,
args: args::QueryRawBytes,
) {
let response = unwrap_client_response::<N::Client, _>(
RPC.shell()
.storage_value(
context.client(),
None,
None,
false,
&args.storage_key,
)
.await,
);
if !response.data.is_empty() {
display_line!(
context.io(),
"Found data: 0x{}",
HEXLOWER.encode(&response.data)
);
} else {
display_line!(
context.io(),
"No data found for key {}",
args.storage_key
);
}
}
pub async fn query_balance(context: &impl Namada, args: args::QueryBalance) {
match &args.owner {
BalanceOwner::Address(_) => {
query_transparent_balance(context, args).await
}
BalanceOwner::FullViewingKey(_) => {
query_shielded_balance(context, args).await
}
}
}
async fn query_transparent_balance(
context: &impl Namada,
args: args::QueryBalance,
) {
let args::QueryBalance {
owner,
token,
height,
..
} = args;
let owner = owner
.address()
.expect("Balance owner should have been a transparent address");
let token_alias = lookup_token_alias(context, &token, Some(&owner)).await;
let token_balance_result = namada_sdk::rpc::get_token_balance(
context.client(),
&token,
&owner,
height,
)
.await;
match token_balance_result {
Ok(balance) => {
let balance = context.format_amount(&token, balance).await;
display_line!(context.io(), "{token_alias}: {balance}");
}
Err(e) => {
display_line!(
context.io(),
"Error querying balance of {token_alias}: {e}"
);
}
}
}
async fn lookup_token_alias(
context: &impl Namada,
token: &Address,
owner: Option<&Address>,
) -> String {
match token {
Address::Internal(InternalAddress::Erc20(eth_addr)) => {
eth_addr.to_canonical()
}
Address::Internal(InternalAddress::IbcToken(_)) => {
let ibc_denom =
rpc::query_ibc_denom(context, token.to_string(), owner).await;
context.wallet().await.lookup_ibc_token_alias(ibc_denom)
}
_ => context
.wallet()
.await
.find_alias(token)
.map(|alias| alias.to_string())
.unwrap_or(token.to_string()),
}
}
pub async fn query_proposal_votes(
context: &impl Namada,
args: args::QueryProposalVotes,
) {
let result = namada_sdk::rpc::query_proposal_votes(
context.client(),
args.proposal_id,
)
.await
.unwrap();
match args.voter {
Some(voter) => {
match result.into_iter().find(|vote| vote.delegator == voter) {
Some(vote) => display_line!(context.io(), "{}", vote,),
None => display_line!(
context.io(),
"The address {} has not voted on proposal {}",
voter,
args.proposal_id
),
}
}
None => {
display_line!(
context.io(),
"Votes for proposal id {}\n",
args.proposal_id
);
for vote in result {
display_line!(context.io(), "{}\n", vote);
}
}
}
}
pub async fn query_proposal(context: &impl Namada, args: args::QueryProposal) {
let current_epoch = query_and_print_epoch(context).await;
if let Some(id) = args.proposal_id {
let proposal =
query_proposal_by_id(context.client(), id).await.unwrap();
if let Some(proposal) = proposal {
display_line!(
context.io(),
"{}",
proposal.to_string_with_status(current_epoch)
);
} else {
edisplay_line!(context.io(), "No proposal found with id: {}", id);
}
} else {
let counter_key = governance_storage::get_counter_key();
let next_proposal_id: u64 =
query_storage_value(context.client(), &counter_key)
.await
.unwrap();
if next_proposal_id == 0 {
display_line!(context.io(), "No proposals found.");
return;
}
let last_proposal_id = next_proposal_id.checked_sub(1).unwrap();
let from_id = last_proposal_id.saturating_sub(10);
for id in from_id..=last_proposal_id {
let proposal = query_proposal_by_id(context.client(), id)
.await
.unwrap()
.expect("Proposal should be written to storage.");
display_line!(context.io(), "{}", proposal);
}
}
}
pub async fn query_proposal_by_id<C: Client + Sync>(
client: &C,
proposal_id: u64,
) -> Result<Option<StorageProposal>, error::Error> {
namada_sdk::rpc::query_proposal_by_id(client, proposal_id).await
}
pub async fn query_rewards_estimate(
context: &impl Namada,
args: args::QueryShieldingRewardsEstimate,
) {
let mut shielded = context.shielded_mut().await;
if let Err(e) = shielded.load_with_caching(context.client()).await {
edisplay_line!(context.io(), "Failed to load shielded context: {}", e);
cli::safe_exit(1);
}
let raw_balance = match shielded
.compute_shielded_balance(&args.owner.as_viewing_key())
.await
{
Ok(balance) => balance,
Err(e) => {
edisplay_line!(
context.io(),
"Failed to query shielded balance: {}",
e
);
cli::safe_exit(1);
}
};
let rewards_estimate = match raw_balance {
Some(balance) => {
match shielded
.estimate_next_epoch_rewards(context, &balance)
.await
{
Ok(estimate) => estimate,
Err(e) => {
edisplay_line!(
context.io(),
"Failed to estimate rewards for the next MASP epoch: \
{}",
e
);
cli::safe_exit(1);
}
}
}
None => DenominatedAmount::native(Amount::zero()),
};
display_line!(
context.io(),
"Estimated native token rewards for the next MASP epoch: {}",
rewards_estimate
);
}
async fn query_shielded_balance(
context: &impl Namada,
args: args::QueryBalance,
) {
display_line!(
context.io(),
"{}: {}\n",
"WARNING".bold().underline().yellow(),
"The resulting balance could be outdated, make sure to run `namadac \
shielded-sync` before querying the balance to get the most recent \
value."
);
let args::QueryBalance {
owner,
token,
no_conversions,
..
} = args;
let viewing_key = ExtendedFullViewingKey::from(
owner
.full_viewing_key()
.expect("Balance owner should have been a masp full viewing key"),
)
.fvk
.vk;
{
let mut shielded = context.shielded_mut().await;
if let Err(e) = shielded.load_with_caching(context.client()).await {
edisplay_line!(
context.io(),
"Failed to load shielded context: {}",
e
);
cli::safe_exit(1);
}
let token_map = context.wallet().await.get_addresses();
let mut tokens: BTreeSet<_> = token_map.values().collect();
tokens.insert(&token);
let _ = shielded
.precompute_asset_types(context.client(), tokens)
.await;
let _ = shielded.save().await;
}
let masp_epoch = query_and_print_masp_epoch(context).await;
let token_alias = lookup_token_alias(context, &token, Some(&MASP)).await;
let mut shielded = context.shielded_mut().await;
let no_balance = || {
display_line!(context.io(), "{token_alias}: 0");
};
let balance = if no_conversions || token != context.native_token() {
let Some(bal) = shielded
.compute_shielded_balance(&viewing_key)
.await
.unwrap()
else {
no_balance();
return;
};
bal
} else {
let Some(bal) = shielded
.compute_exchanged_balance(
context.client(),
context.io(),
&viewing_key,
)
.await
.unwrap()
else {
no_balance();
return;
};
bal
};
let total_balance = shielded
.decode_combine_sum_to_epoch(context.client(), balance, masp_epoch)
.await
.0
.get(&token);
if total_balance.is_zero() {
no_balance();
} else {
display_line!(
context.io(),
"{}: {}",
token_alias,
context.format_amount(&token, total_balance.into()).await
);
}
}
pub async fn query_proposal_result<N: Namada>(
context: &N,
args: args::QueryProposalResult,
) {
let proposal_id = args.proposal_id;
let current_epoch = query_epoch(context.client()).await.unwrap();
let proposal_result =
namada_sdk::rpc::query_proposal_result(context.client(), proposal_id)
.await;
let proposal_query =
namada_sdk::rpc::query_proposal_by_id(context.client(), proposal_id)
.await;
match (proposal_query, proposal_result) {
(Ok(Some(proposal_query)), Ok(Some(proposal_result))) => {
display_line!(context.io(), "Proposal Id: {} ", proposal_id);
if current_epoch >= proposal_query.voting_end_epoch {
display_line!(
context.io(),
"{:4}The voting period has ended.",
""
);
display_line!(context.io(), "{:4}{}", "", proposal_result);
} else {
display_line!(
context.io(),
"{:4}The voting period is underway and will continue \
until epoch {} begins.",
"",
proposal_query.voting_end_epoch,
);
if let Ok(Some(last_epoch)) =
namada_sdk::governance::utils::last_validator_voting_epoch(
proposal_query.voting_start_epoch,
proposal_query.voting_end_epoch,
)
{
display_line!(
context.io(),
"{:4}NOTE: Validators can vote only until the end of \
epoch {}.",
"",
last_epoch
)
}
let res = format!("{}", proposal_result);
if let Some(idx) = res.find(' ') {
let slice = &res[idx..];
display_line!(context.io(), "{:4}Currently{}", "", slice);
} else {
display_line!(
context.io(),
"{:4}Error parsing the result string",
"",
);
}
}
}
(Ok(Some(proposal_query)), Ok(None)) => {
display_line!(context.io(), "Proposal Id: {} ", proposal_id);
display_line!(
context.io(),
"{:4}The voting period has not begun yet.",
""
);
display_line!(
context.io(),
"{:4}Start epoch: {}. End epoch: {}",
"",
proposal_query.voting_start_epoch,
proposal_query.voting_end_epoch
);
if let Ok(Some(last_epoch)) =
namada_sdk::governance::utils::last_validator_voting_epoch(
proposal_query.voting_start_epoch,
proposal_query.voting_end_epoch,
)
{
display_line!(
context.io(),
"{:4}NOTE: Validators will be able to vote only until the \
end of epoch {}.",
"",
last_epoch
)
}
}
_ => {
edisplay_line!(context.io(), "Proposal {} not found.", proposal_id);
}
}
}
pub async fn query_account(context: &impl Namada, args: args::QueryAccount) {
let account = rpc::get_account_info(context.client(), &args.owner)
.await
.unwrap();
if let Some(account) = account {
display_line!(context.io(), "Address: {}", account.address);
display_line!(context.io(), "Threshold: {}", account.threshold);
display_line!(context.io(), "Public keys:");
for (public_key, _) in account.public_keys_map.pk_to_idx {
display_line!(context.io(), "- {}", public_key);
}
} else {
display_line!(context.io(), "No account exists for {}", args.owner);
}
}
pub async fn query_pgf(context: &impl Namada, _args: args::QueryPgf) {
let stewards = query_pgf_stewards(context.client()).await;
let fundings = query_pgf_fundings(context.client()).await;
match stewards.is_empty() {
true => {
display_line!(
context.io(),
"Pgf stewards: no stewards are currently set."
)
}
false => {
display_line!(context.io(), "Pgf stewards:");
for steward in stewards {
display_line!(context.io(), "{:4}- {}", "", steward.address);
display_line!(context.io(), "{:4} Reward distribution:", "");
for (address, percentage) in steward.reward_distribution {
display_line!(
context.io(),
"{:6}- {} to {}",
"",
percentage,
address
);
}
}
}
}
match fundings.is_empty() {
true => {
display_line!(
context.io(),
"Pgf fundings: no fundings are currently set."
)
}
false => {
display_line!(context.io(), "Pgf fundings:");
for funding in fundings {
display_line!(
context.io(),
"{:4}- {} for {}",
"",
funding.detail.target(),
funding.detail.amount().to_string_native()
);
}
}
}
}
pub async fn query_protocol_parameters(
context: &impl Namada,
_args: args::QueryProtocolParameters,
) {
let GovernanceParameters {
min_proposal_fund,
max_proposal_code_size,
min_proposal_voting_period,
max_proposal_period,
max_proposal_content_size,
min_proposal_grace_epochs,
max_proposal_latency,
} = query_governance_parameters(context.client()).await;
display_line!(context.io(), "\nGovernance Parameters");
display_line!(
context.io(),
"{:4}Min. proposal fund: {} native tokens",
"",
min_proposal_fund.to_string_native()
);
display_line!(
context.io(),
"{:4}Max. proposal code size: {} bytes",
"",
max_proposal_code_size
);
display_line!(
context.io(),
"{:4}Min. proposal voting period: {} epochs",
"",
min_proposal_voting_period
);
display_line!(
context.io(),
"{:4}Max. proposal period: {} epochs",
"",
max_proposal_period
);
display_line!(
context.io(),
"{:4}Max. proposal content size: {} characters",
"",
max_proposal_content_size
);
display_line!(
context.io(),
"{:4}Min. proposal grace epochs: {}",
"",
min_proposal_grace_epochs
);
display_line!(
context.io(),
"{:4}Max. proposal latency: {} epochs",
"",
max_proposal_latency
);
let PgfParameters {
stewards: _,
pgf_inflation_rate,
stewards_inflation_rate,
maximum_number_of_stewards,
} = query_pgf_parameters(context.client()).await;
display_line!(context.io(), "\nPublic Goods Funding Parameters");
display_line!(
context.io(),
"{:4}Pgf inflation rate: {}",
"",
pgf_inflation_rate
);
display_line!(
context.io(),
"{:4}Steward inflation rate: {}",
"",
stewards_inflation_rate
);
display_line!(
context.io(),
"{:4}Max. number of stewards: {}",
"",
maximum_number_of_stewards
);
display_line!(context.io(), "\nProtocol parameters");
let key = param_storage::get_epoch_duration_storage_key();
let epoch_duration: EpochDuration =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_masp_epoch_multiplier_key();
let masp_epoch_multiplier: u64 =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_implicit_vp_key();
let implicit_vp_code_hash: Hash =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_vp_allowlist_storage_key();
let vp_allowlist: Vec<String> = query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_tx_allowlist_storage_key();
let tx_allowlist: Vec<String> = query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_max_proposal_bytes_key();
let max_proposal_bytes: ProposalBytes =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_max_tx_bytes_key();
let max_tx_bytes: u32 = query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_max_block_gas_key();
let max_block_gas: u64 = query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_masp_fee_payment_gas_limit_key();
let masp_fee_payment_gas_limit: u64 =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_gas_cost_key();
let minimum_gas_price: BTreeMap<Address, token::Amount> =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_gas_scale_key();
let gas_scale: u64 = query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_native_token_transferable_key();
let is_native_token_transferable: bool =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_epochs_per_year_key();
let epochs_per_year: u64 = query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let parameters = namada_core::parameters::Parameters {
max_tx_bytes,
epoch_duration,
max_proposal_bytes,
max_block_gas,
vp_allowlist,
tx_allowlist,
implicit_vp_code_hash: Some(implicit_vp_code_hash),
epochs_per_year,
masp_epoch_multiplier,
masp_fee_payment_gas_limit,
gas_scale,
minimum_gas_price,
is_native_token_transferable,
};
let namada_core::parameters::Parameters {
max_tx_bytes,
epoch_duration,
max_proposal_bytes,
max_block_gas,
vp_allowlist,
tx_allowlist,
implicit_vp_code_hash,
epochs_per_year,
masp_epoch_multiplier,
masp_fee_payment_gas_limit,
gas_scale,
minimum_gas_price,
is_native_token_transferable,
} = parameters;
display_line!(
context.io(),
"{:4}Min. epoch duration: {} seconds",
"",
epoch_duration.min_duration
);
display_line!(context.io(), "{:4}Epochs per year: {}", "", epochs_per_year);
display_line!(
context.io(),
"{:4}Min. number of blocks: {}",
"",
epoch_duration.min_num_of_blocks
);
display_line!(
context.io(),
"{:4}Masp epoch multiplier: {:?}",
"",
masp_epoch_multiplier
);
display_line!(
context.io(),
"{:4}Implicit VP hash: {}",
"",
implicit_vp_code_hash.expect("The implicit vp should be set")
);
display_line!(context.io(), "{:4}VP allowlist: {:?}", "", vp_allowlist);
display_line!(
context.io(),
"{:4}Transactions allowlist: {:?}",
"",
tx_allowlist
);
display_line!(
context.io(),
"{:4}Max. proposal bytes: {:?}",
"",
max_proposal_bytes.get()
);
display_line!(context.io(), "{:4}Max tx bytes: {:?}", "", max_tx_bytes);
display_line!(
context.io(),
"{:4}Max. block gas: {:?} gas units",
"",
max_block_gas
);
display_line!(
context.io(),
"{:4}Masp fee payment gas limit: {:?} gas units",
"",
masp_fee_payment_gas_limit
);
display_line!(context.io(), "{:4}Minimum gas costs:", "");
for (token, gas_cost) in minimum_gas_price {
let denom = rpc::query_denom(context.client(), &token)
.await
.expect("Token should have denom");
let den_amt = DenominatedAmount::new(gas_cost, denom);
display_line!(
context.io(),
"{:8}{}: {} per gas unit",
"",
&token,
den_amt
);
}
display_line!(context.io(), "{:4}Gas scale: {:?}", "", gas_scale);
display_line!(
context.io(),
"{:4}Is native token transferable: {:?}",
"",
is_native_token_transferable
);
display_line!(context.io(), "\nProof of Stake parameters");
let PosParams {
owned:
OwnedPosParams {
max_validator_slots,
pipeline_len,
unbonding_len,
tm_votes_per_token,
block_proposer_reward,
block_vote_reward,
max_inflation_rate,
target_staked_ratio,
duplicate_vote_min_slash_rate,
light_client_attack_min_slash_rate,
cubic_slashing_window_length,
validator_stake_threshold,
liveness_window_check,
liveness_threshold,
rewards_gain_p,
rewards_gain_d,
},
max_proposal_period: _,
} = query_pos_parameters(context.client()).await;
display_line!(
context.io(),
"{:4}Pipeline length: {} epochs",
"",
pipeline_len
);
display_line!(
context.io(),
"{:4}Unbonding length: {} epochs",
"",
unbonding_len
);
display_line!(
context.io(),
"{:4}Cubic slashing window length: {} epoch",
"",
cubic_slashing_window_length
);
display_line!(
context.io(),
"{:4}Max. consensus validator slots: {}",
"",
max_validator_slots
);
display_line!(
context.io(),
"{:4}Validator stake threshold: {} native tokens",
"",
validator_stake_threshold.to_string_native()
);
display_line!(
context.io(),
"{:4}Duplicate vote minimum slash rate: {}",
"",
duplicate_vote_min_slash_rate
);
display_line!(
context.io(),
"{:4}Light client attack minimum slash rate: {}",
"",
light_client_attack_min_slash_rate
);
display_line!(
context.io(),
"{:4}Liveness window: {} blocks",
"",
liveness_window_check
);
display_line!(
context.io(),
"{:4}Liveness threshold: {}",
"",
liveness_threshold
);
display_line!(
context.io(),
"{:4}Block proposer reward: {}",
"",
block_proposer_reward
);
display_line!(
context.io(),
"{:4}Block vote reward: {}",
"",
block_vote_reward
);
display_line!(
context.io(),
"{:4}Max inflation rate: {}",
"",
max_inflation_rate
);
display_line!(
context.io(),
"{:4}Target staked ratio: {}",
"",
target_staked_ratio
);
display_line!(
context.io(),
"{:4}Inflation kP gain: {}",
"",
rewards_gain_p
);
display_line!(
context.io(),
"{:4}Inflation kD gain: {}",
"",
rewards_gain_d
);
display_line!(
context.io(),
"{:4}Votes per raw native token: {}",
"",
tm_votes_per_token
);
display_line!(context.io(), "\nIBC parameters");
let IbcParameters {
default_rate_limits:
IbcTokenRateLimits {
mint_limit,
throughput_per_epoch_limit,
},
} = query_ibc_params(context.client())
.await
.expect("Failed to query the IBC parameters");
display_line!(context.io(), "{:4}Default mint limit: {}", "", mint_limit);
display_line!(
context.io(),
"{:4}Default per epoch throughput limit: {}",
"",
throughput_per_epoch_limit
);
}
pub async fn query_bond<C: Client + Sync>(
client: &C,
source: &Address,
validator: &Address,
epoch: Option<Epoch>,
) -> token::Amount {
unwrap_client_response::<C, token::Amount>(
RPC.vp().pos().bond(client, source, validator, &epoch).await,
)
}
pub async fn query_unbond_with_slashing<C: Client + Sync>(
client: &C,
source: &Address,
validator: &Address,
) -> HashMap<(Epoch, Epoch), token::Amount> {
unwrap_client_response::<C, HashMap<(Epoch, Epoch), token::Amount>>(
RPC.vp()
.pos()
.unbond_with_slashing(client, source, validator)
.await,
)
}
pub async fn query_pos_parameters<C: Client + Sync>(client: &C) -> PosParams {
unwrap_client_response::<C, PosParams>(
RPC.vp().pos().pos_params(client).await,
)
}
pub async fn query_consensus_keys<C: Client + Sync>(
client: &C,
) -> BTreeSet<common::PublicKey> {
unwrap_client_response::<C, BTreeSet<common::PublicKey>>(
RPC.vp().pos().consensus_key_set(client).await,
)
}
pub async fn query_pgf_stewards<C: Client + Sync>(
client: &C,
) -> Vec<StewardDetail> {
unwrap_client_response::<C, _>(RPC.vp().pgf().stewards(client).await)
}
pub async fn query_pgf_fundings<C: Client + Sync>(
client: &C,
) -> Vec<StoragePgfFunding> {
unwrap_client_response::<C, _>(RPC.vp().pgf().funding(client).await)
}
pub async fn query_pgf_parameters<C: Client + Sync>(
client: &C,
) -> PgfParameters {
unwrap_client_response::<C, _>(RPC.vp().pgf().parameters(client).await)
}
pub async fn query_and_print_unbonds(
context: &impl Namada,
source: &Address,
validator: &Address,
) {
let unbonds =
query_unbond_with_slashing(context.client(), source, validator).await;
let current_epoch = query_epoch(context.client()).await.unwrap();
let mut total_withdrawable = token::Amount::zero();
let mut not_yet_withdrawable = HashMap::<Epoch, token::Amount>::new();
for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() {
if withdraw_epoch <= current_epoch {
total_withdrawable =
total_withdrawable.checked_add(amount).unwrap();
} else {
let withdrawable_amount =
not_yet_withdrawable.entry(withdraw_epoch).or_default();
*withdrawable_amount =
withdrawable_amount.checked_add(amount).unwrap();
}
}
if !total_withdrawable.is_zero() {
display_line!(
context.io(),
"Total withdrawable now: {}.",
total_withdrawable.to_string_native()
);
}
if !not_yet_withdrawable.is_empty() {
display_line!(context.io(), "Current epoch: {current_epoch}.");
}
for (withdraw_epoch, amount) in not_yet_withdrawable {
display_line!(
context.io(),
"Amount {} withdrawable starting from epoch {withdraw_epoch}.",
amount.to_string_native(),
);
}
}
pub async fn query_withdrawable_tokens<C: Client + Sync>(
client: &C,
bond_source: &Address,
validator: &Address,
epoch: Option<Epoch>,
) -> token::Amount {
unwrap_client_response::<C, token::Amount>(
RPC.vp()
.pos()
.withdrawable_tokens(client, bond_source, validator, &epoch)
.await,
)
}
pub async fn query_bonds(
context: &impl Namada,
args: args::QueryBonds,
) -> std::io::Result<()> {
let epoch = query_and_print_epoch(context).await;
let source = args.owner;
let validator = args.validator;
let stdout = io::stdout();
let mut w = stdout.lock();
let bonds_and_unbonds = enriched_bonds_and_unbonds(
context.client(),
epoch,
&source,
&validator,
)
.await
.unwrap();
for (bond_id, details) in &bonds_and_unbonds.data {
let bond_type = if bond_id.source == bond_id.validator {
format!("Self-bonds from {}", bond_id.validator)
} else {
format!(
"Delegations from {} to {}",
bond_id.source, bond_id.validator
)
};
display_line!(context.io(), &mut w; "{}:", bond_type)?;
for bond in &details.data.bonds {
display_line!(
context.io(),
&mut w;
" Remaining active bond from epoch {}: Δ {} (slashed {})",
bond.start,
bond.amount.to_string_native(),
bond.slashed_amount.unwrap_or_default().to_string_native()
)?;
}
if !details.bonds_total.is_zero() {
display_line!(
context.io(),
&mut w;
"Active (slashable) bonds total: {}",
details.bonds_total_active().unwrap().to_string_native()
)?;
}
display_line!(context.io(), &mut w; "Bonds total: {}", details.bonds_total.to_string_native())?;
display_line!(context.io(), &mut w; "")?;
if !details.data.unbonds.is_empty() {
let bond_type = if bond_id.source == bond_id.validator {
format!("Unbonded self-bonds from {}", bond_id.validator)
} else {
format!("Unbonded delegations from {}", bond_id.source)
};
display_line!(context.io(), &mut w; "{}:", bond_type)?;
for unbond in &details.data.unbonds {
display_line!(
context.io(),
&mut w;
" Withdrawable from epoch {} (active from {}): Δ {} (slashed {})",
unbond.withdraw,
unbond.start,
unbond.amount.to_string_native(),
unbond.slashed_amount.unwrap_or_default().to_string_native()
)?;
}
display_line!(
context.io(),
&mut w;
"Unbonded total: {}",
details.unbonds_total.to_string_native()
)?;
}
display_line!(
context.io(),
&mut w;
"Withdrawable total: {}",
details.total_withdrawable.to_string_native()
)?;
display_line!(context.io(), &mut w; "")?;
}
if bonds_and_unbonds.bonds_total != bonds_and_unbonds.bonds_total_slashed {
display_line!(
context.io(),
&mut w;
"All bonds total active: {}",
bonds_and_unbonds.bonds_total_active().unwrap().to_string_native()
)?;
}
display_line!(
context.io(),
&mut w;
"All bonds total: {}",
bonds_and_unbonds.bonds_total.to_string_native()
)?;
display_line!(
context.io(),
&mut w;
"All bonds total slashed: {}",
bonds_and_unbonds.bonds_total_slashed.to_string_native()
)?;
if bonds_and_unbonds.unbonds_total
!= bonds_and_unbonds.unbonds_total_slashed
{
display_line!(
context.io(),
&mut w;
"All unbonds total active: {}",
bonds_and_unbonds.unbonds_total_active().unwrap().to_string_native()
)?;
}
display_line!(
context.io(),
&mut w;
"All unbonds total: {}",
bonds_and_unbonds.unbonds_total.to_string_native()
)?;
display_line!(
context.io(),
&mut w;
"All unbonds total withdrawable: {}",
bonds_and_unbonds.total_withdrawable.to_string_native()
)?;
display_line!(
context.io(),
&mut w;
"All unbonds total slashed: {}",
bonds_and_unbonds.unbonds_total_slashed.to_string_native()
)?;
Ok(())
}
pub async fn query_bonded_stake<N: Namada>(
context: &N,
args: args::QueryBondedStake,
) {
let epoch = match args.epoch {
Some(epoch) => epoch,
None => query_and_print_epoch(context).await,
};
match args.validator {
Some(validator) => {
let stake =
get_validator_stake(context.client(), epoch, &validator).await;
match stake {
Some(stake) => {
display_line!(
context.io(),
"Bonded stake of validator {validator}: {}",
stake.to_string_native()
);
query_and_print_validator_state(
context,
args::QueryValidatorState {
query: args.query,
validator,
epoch: args.epoch,
},
)
.await;
}
None => {
display_line!(
context.io(),
"No bonded stake found for {validator}"
);
}
}
}
None => {
let consensus: BTreeSet<WeightedValidator> =
unwrap_client_response::<N::Client, _>(
RPC.vp()
.pos()
.consensus_validator_set(context.client(), &Some(epoch))
.await,
);
let below_capacity: BTreeSet<WeightedValidator> =
unwrap_client_response::<N::Client, _>(
RPC.vp()
.pos()
.below_capacity_validator_set(
context.client(),
&Some(epoch),
)
.await,
);
let stdout = io::stdout();
let mut w = stdout.lock();
display_line!(context.io(), &mut w; "Consensus validators:")
.unwrap();
for val in consensus.into_iter().rev() {
display_line!(
context.io(),
&mut w;
" {}: {}",
val.address.encode(),
val.bonded_stake.to_string_native()
)
.unwrap();
}
if !below_capacity.is_empty() {
display_line!(context.io(), &mut w; "Below capacity validators:")
.unwrap();
for val in below_capacity.into_iter().rev() {
display_line!(
context.io(),
&mut w;
" {}: {}",
val.address.encode(),
val.bonded_stake.to_string_native()
)
.unwrap();
}
}
}
}
let total_staked_tokens =
get_total_staked_tokens(context.client(), epoch).await;
display_line!(
context.io(),
"Total bonded stake: {}",
total_staked_tokens.to_string_native()
);
}
pub async fn query_commission_rate<C: Client + Sync>(
client: &C,
validator: &Address,
epoch: Option<Epoch>,
) -> CommissionPair {
unwrap_client_response::<C, CommissionPair>(
RPC.vp()
.pos()
.validator_commission(client, validator, &epoch)
.await,
)
}
pub async fn query_metadata<C: Client + Sync>(
client: &C,
validator: &Address,
) -> Option<ValidatorMetaData> {
unwrap_client_response::<C, Option<ValidatorMetaData>>(
RPC.vp().pos().validator_metadata(client, validator).await,
)
}
pub async fn query_validator_state<C: Client + Sync>(
client: &C,
validator: &Address,
epoch: Option<Epoch>,
) -> ValidatorStateInfo {
unwrap_client_response::<C, ValidatorStateInfo>(
RPC.vp()
.pos()
.validator_state(client, validator, &epoch)
.await,
)
}
pub async fn query_rewards<C: Client + Sync>(
client: &C,
source: &Option<Address>,
validator: &Address,
epoch: &Option<Epoch>,
) -> token::Amount {
unwrap_sdk_result(
rpc::query_rewards(client, source, validator, epoch).await,
)
}
pub async fn query_total_supply<N: Namada>(
context: &N,
args: args::QueryTotalSupply,
) {
let token = args.token;
let supply = unwrap_sdk_result(
rpc::get_token_total_supply(context.client(), &token).await,
);
let amount_str = format_denominated_amount(
context.client(),
context.io(),
&token,
supply,
)
.await;
display_line!(
context.io(),
"Total supply of token {token}: {}",
amount_str
);
}
pub async fn query_effective_native_supply<N: Namada>(context: &N) {
let native_supply = unwrap_client_response::<N::Client, token::Amount>(
RPC.vp()
.token()
.effective_native_supply(context.client())
.await,
);
display_line!(context.io(), "nam: {}", native_supply.to_string_native());
}
pub async fn query_staking_rewards_rate<N: Namada>(context: &N) {
let PosRewardsRates {
staking_rewards_rate,
inflation_rate,
} = unwrap_client_response::<N::Client, PosRewardsRates>(
RPC.vp()
.token()
.staking_rewards_rate(context.client())
.await,
);
let last_staked_ratio_key =
proof_of_stake::storage_key::last_staked_ratio_key();
let last_staked_ratio: Dec =
query_storage_value(context.client(), &last_staked_ratio_key)
.await
.unwrap();
display_line!(context.io(), "Last staked ratio: {last_staked_ratio}");
if staking_rewards_rate.is_zero() && inflation_rate.is_zero() {
display_line!(
context.io(),
"No PoS inflation and rewards tokens were minted."
);
} else {
display_line!(
context.io(),
"Current annual staking rewards rate: {}\nCurrent PoS inflation \
rate: {}",
staking_rewards_rate,
inflation_rate
);
}
}
pub async fn query_and_print_validator_state(
context: &impl Namada,
args: args::QueryValidatorState,
) {
let validator = args.validator;
let (state, epoch): ValidatorStateInfo =
query_validator_state(context.client(), &validator, args.epoch).await;
match state {
Some(state) => match state {
ValidatorState::Consensus => {
display_line!(
context.io(),
"Validator {validator} is in the consensus set in epoch \
{epoch}"
)
}
ValidatorState::BelowCapacity => {
display_line!(
context.io(),
"Validator {validator} is in the below-capacity set in \
epoch {epoch}"
)
}
ValidatorState::BelowThreshold => {
display_line!(
context.io(),
"Validator {validator} is in the below-threshold set in \
epoch {epoch}"
)
}
ValidatorState::Inactive => {
display_line!(
context.io(),
"Validator {validator} is inactive in epoch {epoch}"
)
}
ValidatorState::Jailed => {
display_line!(
context.io(),
"Validator {validator} is jailed in epoch {epoch}"
)
}
},
None => display_line!(
context.io(),
"Validator {validator} not found in epoch {epoch}. This account \
may not be a validator, or the validator account has been \
recently initialized and may not be active yet. It is also \
possible that this data is no longer available in storage if an \
epoch before the current epoch has been queried."
),
}
}
pub async fn query_and_print_commission_rate(
context: &impl Namada,
args: args::QueryCommissionRate,
) {
let validator = args.validator;
let CommissionPair {
commission_rate,
max_commission_change_per_epoch,
epoch: query_epoch,
} = query_commission_rate(context.client(), &validator, args.epoch).await;
match (commission_rate, max_commission_change_per_epoch) {
(Some(commission_rate), Some(max_commission_change_per_epoch)) => {
display_line!(
context.io(),
"Validator {validator} commission rate: {commission_rate}, \
max change per epoch: {max_commission_change_per_epoch} in \
epoch {query_epoch}"
)
}
(None, None) => display_line!(
context.io(),
"Validator {validator} not found in epoch {query_epoch}. This \
account may not be a validator, or the validator account has \
been recently initialized and may not be active yet. It is also \
possible that this data is no longer available in storage if an \
epoch before the current epoch has been queried."
),
_ => display_line!(
context.io(),
"Only one of the commission rate and max commission change per \
epoch was found for validator {validator} in epoch \
{query_epoch}. This is a bug and should be reported."
),
}
}
pub async fn query_and_print_metadata(
context: &impl Namada,
args: args::QueryMetaData,
) {
let validator = args.validator;
let metadata: Option<ValidatorMetaData> =
query_metadata(context.client(), &validator).await;
match metadata {
Some(ValidatorMetaData {
email,
description,
website,
discord_handle,
avatar,
name,
}) => {
display_line!(
context.io(),
"Validator {} metadata:",
validator.encode()
);
if let Some(name) = name {
display_line!(context.io(), "Validator name: {}", name);
} else {
display_line!(context.io(), "No validator name");
}
display_line!(context.io(), "Email: {}", email);
if let Some(description) = description {
display_line!(context.io(), "Description: {}", description);
} else {
display_line!(context.io(), "No description");
}
if let Some(website) = website {
display_line!(context.io(), "Website: {}", website);
} else {
display_line!(context.io(), "No website");
}
if let Some(discord_handle) = discord_handle {
display_line!(
context.io(),
"Discord handle: {}",
discord_handle
);
} else {
display_line!(context.io(), "No discord handle");
}
if let Some(avatar) = avatar {
display_line!(context.io(), "Avatar: {}", avatar);
} else {
display_line!(context.io(), "No avatar");
}
}
None => display_line!(
context.io(),
"Validator {} does not have an email set and may not exist",
validator.encode()
),
}
let CommissionPair {
commission_rate,
max_commission_change_per_epoch,
epoch: query_epoch,
} = query_commission_rate(context.client(), &validator, None).await;
match (commission_rate, max_commission_change_per_epoch) {
(Some(commission_rate), Some(max_commission_change_per_epoch)) => {
display_line!(
context.io(),
"Validator {validator} commission rate: {commission_rate}, \
max change per epoch: {max_commission_change_per_epoch} in \
epoch {query_epoch}"
)
}
(None, None) => display_line!(
context.io(),
"Validator {validator} not found in epoch {query_epoch}. This \
account may not be a validator, or the validator account has \
been recently initialized and may not be active yet. It is also \
possible that this data is no longer available in storage if an \
epoch before the current epoch has been queried."
),
_ => display_line!(
context.io(),
"Only one of the commission rate and max commission change per \
epoch was found for validator {validator} in epoch \
{query_epoch}. This is a bug and should be reported."
),
}
}
pub async fn query_slashes<N: Namada>(context: &N, args: args::QuerySlashes) {
match args.validator {
Some(validator) => {
let slashes: Vec<Slash> = unwrap_client_response::<N::Client, _>(
RPC.vp()
.pos()
.validator_slashes(context.client(), &validator)
.await,
);
if !slashes.is_empty() {
display_line!(context.io(), "Processed slashes:");
let stdout = io::stdout();
let mut w = stdout.lock();
for slash in slashes {
display_line!(
context.io(),
&mut w;
"Infraction epoch {}, block height {}, type {}, rate \
{}",
slash.epoch,
slash.block_height,
slash.r#type,
slash.rate
)
.unwrap();
}
} else {
display_line!(
context.io(),
"No processed slashes found for {}",
validator.encode()
)
}
let enqueued_slashes: HashMap<
Address,
BTreeMap<Epoch, Vec<Slash>>,
> = unwrap_client_response::<N::Client, _>(
RPC.vp().pos().enqueued_slashes(context.client()).await,
);
let enqueued_slashes = enqueued_slashes.get(&validator).cloned();
if let Some(enqueued) = enqueued_slashes {
display_line!(
context.io(),
"\nEnqueued slashes for future processing"
);
for (epoch, slashes) in enqueued {
display_line!(
context.io(),
"To be processed in epoch {}",
epoch
);
for slash in slashes {
let stdout = io::stdout();
let mut w = stdout.lock();
display_line!(
context.io(),
&mut w;
"Infraction epoch {}, block height {}, type {}",
slash.epoch, slash.block_height, slash.r#type,
)
.unwrap();
}
}
} else {
display_line!(
context.io(),
"No enqueued slashes found for {}",
validator.encode()
)
}
}
None => {
let all_slashes: HashMap<Address, Vec<Slash>> =
unwrap_client_response::<N::Client, _>(
RPC.vp().pos().slashes(context.client()).await,
);
if !all_slashes.is_empty() {
let stdout = io::stdout();
let mut w = stdout.lock();
display_line!(context.io(), "Processed slashes:");
for (validator, slashes) in all_slashes.into_iter() {
for slash in slashes {
display_line!(
context.io(),
&mut w;
"Infraction epoch {}, block height {}, rate {}, \
type {}, validator {}",
slash.epoch,
slash.block_height,
slash.rate,
slash.r#type,
validator,
)
.unwrap();
}
}
} else {
display_line!(context.io(), "No processed slashes found")
}
let enqueued_slashes: HashMap<
Address,
BTreeMap<Epoch, Vec<Slash>>,
> = unwrap_client_response::<N::Client, _>(
RPC.vp().pos().enqueued_slashes(context.client()).await,
);
if !enqueued_slashes.is_empty() {
display_line!(
context.io(),
"\nEnqueued slashes for future processing"
);
for (validator, slashes_by_epoch) in enqueued_slashes {
for (epoch, slashes) in slashes_by_epoch {
display_line!(
context.io(),
"\nTo be processed in epoch {}",
epoch
);
for slash in slashes {
let stdout = io::stdout();
let mut w = stdout.lock();
display_line!(
context.io(),
&mut w;
"Infraction epoch {}, block height {}, type \
{}, validator {}",
slash.epoch,
slash.block_height,
slash.r#type,
validator
)
.unwrap();
}
}
}
} else {
display_line!(
context.io(),
"\nNo enqueued slashes found for future processing"
)
}
}
}
}
pub async fn query_and_print_rewards<N: Namada>(
context: &N,
args: args::QueryRewards,
) {
let (source, validator, epoch) = (args.source, args.validator, args.epoch);
let rewards =
query_rewards(context.client(), &source, &validator, &epoch).await;
display_line!(
context.io(),
"{}: {} NAM",
epoch
.map(|e| format!("Rewards at epoch {}", e))
.unwrap_or_else(
|| "Current rewards available for claim".to_string()
),
rewards.to_string_native()
);
}
pub async fn query_delegations<N: Namada>(
context: &N,
args: args::QueryDelegations,
) {
let owner = args.owner;
let delegations: HashSet<Address> = unwrap_client_response::<N::Client, _>(
RPC.vp()
.pos()
.delegation_validators(context.client(), &owner, &None)
.await,
);
if delegations.is_empty() {
display_line!(
context.io(),
"No delegations found active in the current epoch"
);
} else {
display_line!(
context.io(),
"Found delegations in the current epoch to:"
);
for delegation in delegations {
display_line!(context.io(), " {delegation}");
}
}
}
pub async fn query_find_validator<N: Namada>(
context: &N,
args: args::QueryFindValidator,
) {
let args::QueryFindValidator { query: _, addr } = args;
let validator_addr = match addr {
Either::Left(comet_addr) => {
if comet_addr.len() != 40 {
edisplay_line!(
context.io(),
"Expected 40 characters in Tendermint address, got {}",
comet_addr.len()
);
cli::safe_exit(1);
}
let tm_addr = comet_addr.to_ascii_uppercase();
let validator = unwrap_client_response::<N::Client, _>(
RPC.vp()
.pos()
.validator_by_tm_addr(context.client(), &tm_addr)
.await,
);
match validator {
Some(address) => {
display_line!(
context.io(),
"Found validator address \"{address}\"."
);
address
}
None => {
edisplay_line!(
context.io(),
"No validator with Tendermint address {tm_addr} found."
);
cli::safe_exit(1);
}
}
}
Either::Right(validator_addr) => validator_addr,
};
if let Some(consensus_key) = unwrap_client_response::<N::Client, _>(
RPC.vp()
.pos()
.consensus_key(context.client(), &validator_addr)
.await,
) {
let pkh: PublicKeyHash = (&consensus_key).into();
display_line!(context.io(), "Consensus key: {consensus_key}");
display_line!(
context.io(),
"Tendermint key: {}",
tm_consensus_key_raw_hash(&consensus_key)
);
display_line!(context.io(), "Consensus key hash: {}", pkh);
} else {
display_line!(
context.io(),
"Consensus key for validator {validator_addr} could not be found."
)
}
}
pub async fn get_public_key<C: Client + Sync>(
client: &C,
address: &Address,
index: u8,
) -> Result<Option<common::PublicKey>, error::Error> {
rpc::get_public_key_at(client, address, index).await
}
pub async fn is_validator<C: Client + Sync>(
client: &C,
address: &Address,
) -> bool {
namada_sdk::rpc::is_validator(client, address)
.await
.unwrap()
}
pub async fn is_delegator<C: Client + Sync>(
client: &C,
address: &Address,
) -> bool {
namada_sdk::rpc::is_delegator(client, address)
.await
.unwrap()
}
pub async fn is_delegator_at<C: Client + Sync>(
client: &C,
address: &Address,
epoch: Epoch,
) -> bool {
namada_sdk::rpc::is_delegator_at(client, address, epoch)
.await
.unwrap()
}
pub async fn has_bonds<C: Client + Sync>(
client: &C,
address: &Address,
) -> bool {
namada_sdk::rpc::has_bonds(client, address).await.unwrap()
}
pub async fn known_address<C: Client + Sync>(
client: &C,
address: &Address,
) -> bool {
namada_sdk::rpc::known_address(client, address)
.await
.unwrap()
}
pub async fn query_conversions(
context: &impl Namada,
args: args::QueryConversions,
) {
let target_token = args.token;
if target_token.as_ref().is_none() {
let total_rewards = rpc::query_masp_total_rewards(context.client())
.await
.expect("MASP total rewards should be present");
display_line!(
context.io(),
"Total rewards of native token minted for shielded pool: {}",
total_rewards.to_string_native()
);
}
let tokens = context
.wallet()
.await
.get_addresses_with_vp_type(AddressVpType::Token);
let from = MaspEpoch::zero();
let to = rpc::query_masp_epoch(context.client())
.await
.expect("Unable to query current MASP epoch");
let epochs: Vec<_> = MaspEpoch::iter_bounds_inclusive(from, to).collect();
let conversion_tasks = epochs
.iter()
.map(|epoch| rpc::query_conversions(context.client(), epoch));
let conversions = futures::stream::iter(conversion_tasks)
.buffer_unordered(100)
.fold(BTreeMap::default(), async |mut acc, conversion| {
acc.append(&mut conversion.expect("Conversion should be defined"));
acc
})
.await;
if args.dump_tree {
display_line!(context.io(), "Conversions: {conversions:?}");
}
let mut conversions_found = false;
for (addr, _denom, digit, epoch, amt) in conversions.values() {
if matches!(&target_token, Some(target) if target != addr)
|| matches!(&args.epoch, Some(target) if target != epoch)
|| amt.is_zero()
{
continue;
}
conversions_found = true;
display!(
context.io(),
"{}*2^{}[{}]: ",
tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()),
*digit as u8 * 64,
epoch,
);
let mut prefix = "";
for (asset_type, val) in amt.components() {
let (addr, _denom, digit, epoch, _) = &conversions[asset_type];
display!(
context.io(),
"{}{} {}*2^{}[{}]",
prefix,
val,
tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()),
*digit as u8 * 64,
epoch
);
prefix = " + ";
}
display_line!(context.io(), " = 0");
}
if !conversions_found {
display_line!(
context.io(),
"\nNo conversions found satisfying specified criteria."
);
}
}
pub async fn query_conversion<C: Client + Sync>(
client: &C,
asset_type: AssetType,
) -> Option<(
Address,
token::Denomination,
MaspDigitPos,
MaspEpoch,
I128Sum,
MerklePath<Node>,
)> {
namada_sdk::rpc::query_conversion(client, asset_type).await
}
pub async fn query_masp_reward_tokens(context: &impl Namada) {
let tokens = namada_sdk::rpc::query_masp_reward_tokens(context.client())
.await
.expect("The tokens that may earn MASP rewards should be defined");
display_line!(context.io(), "The following tokens may earn MASP rewards:");
for MaspTokenRewardData {
name,
address,
max_reward_rate,
kp_gain,
kd_gain,
locked_amount_target,
} in tokens
{
display_line!(context.io(), "\n{}: {}", name, address);
display_line!(context.io(), " Max reward rate: {}", max_reward_rate);
display_line!(context.io(), " Kp gain: {}", kp_gain);
display_line!(context.io(), " Kd gain: {}", kd_gain);
display_line!(
context.io(),
" Locked amount target (raw): {}",
locked_amount_target
);
let key = token::storage_key::masp_reward_precision_key(&address);
let precision = query_storage_value::<_, u128>(context.client(), &key)
.await
.expect("Precision should be present");
display_line!(context.io(), " Reward precision: {}", precision);
if !max_reward_rate.is_zero() {
let last_inflation = query_storage_value::<_, token::Amount>(
context.client(),
&token::storage_key::masp_last_inflation_key(&address),
)
.await
.expect("Last inflation should be present");
let key = param_storage::get_masp_epoch_multiplier_key();
let masp_epoch_multiplier: u64 =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let key = param_storage::get_epochs_per_year_key();
let epochs_per_year: u64 =
query_storage_value(context.client(), &key)
.await
.expect("Parameter should be defined.");
let eff_supply = get_effective_native_supply(context.client())
.await
.expect("Failed to get effective native supply");
let last_apy = Dec::try_from(last_inflation)
.expect("Failed to convert last inflation to Dec")
.checked_mul(Dec::from(epochs_per_year / masp_epoch_multiplier))
.unwrap()
.checked_div(
Dec::try_from(eff_supply).expect(
"Failed to convert effective native supply to Dec",
),
)
.unwrap();
display_line!(
context.io(),
" ** Last annual reward rate: {}",
last_apy
);
}
}
}
pub async fn query_wasm_code_hash(
context: &impl Namada,
code_path: impl AsRef<str>,
) -> Result<Hash, error::Error> {
rpc::query_wasm_code_hash(context, code_path).await
}
pub async fn query_storage_value<C: Client + Sync, T>(
client: &C,
key: &storage::Key,
) -> Result<T, error::Error>
where
T: BorshDeserialize,
{
namada_sdk::rpc::query_storage_value(client, key).await
}
pub async fn query_storage_value_bytes<C: Client + Sync>(
client: &C,
key: &storage::Key,
height: Option<BlockHeight>,
prove: bool,
) -> (Option<Vec<u8>>, Option<ProofOps>) {
namada_sdk::rpc::query_storage_value_bytes(client, key, height, prove)
.await
.unwrap()
}
pub async fn query_storage_prefix<'b, T>(
context: &'b impl Namada,
key: &storage::Key,
) -> Option<impl 'b + Iterator<Item = (storage::Key, T)>>
where
T: BorshDeserialize,
{
rpc::query_storage_prefix(context, key).await.unwrap()
}
pub async fn query_has_storage_key<C: Client + Sync>(
client: &C,
key: &storage::Key,
) -> bool {
namada_sdk::rpc::query_has_storage_key(client, key)
.await
.unwrap()
}
pub async fn query_tx_events<C: Client + Sync>(
client: &C,
tx_event_query: namada_sdk::rpc::TxEventQuery<'_>,
) -> std::result::Result<Option<TxAppliedEvents>, <C as Client>::Error> {
namada_sdk::rpc::query_tx_events(client, tx_event_query).await
}
pub async fn query_result(context: &impl Namada, args: args::QueryResult) {
display_line!(
context.io(),
"Checking if tx {} is applied...",
args.tx_hash
);
match rpc::query_tx_status(
context,
namada_sdk::rpc::TxEventQuery::Applied(&args.tx_hash),
Instant::now() + Duration::from_secs(10),
)
.await
{
Ok(events) => {
let resp = match TxResponse::try_from(events) {
Ok(resp) => resp,
Err(err) => {
edisplay_line!(context.io(), "{err}");
cli::safe_exit(1)
}
};
display_batch_resp(context, &resp);
}
Err(err) => {
edisplay_line!(context.io(), "{}", err);
cli::safe_exit(1)
}
}
}
pub async fn epoch_sleep(context: &impl Namada, _args: args::Query) {
let start_epoch = query_and_print_epoch(context).await;
loop {
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
let current_epoch = query_epoch(context.client()).await.unwrap();
if current_epoch > start_epoch {
display_line!(context.io(), "Reached epoch {}", current_epoch);
break;
}
}
}
pub async fn get_bond_amount_at<C: Client + Sync>(
client: &C,
delegator: &Address,
validator: &Address,
epoch: Epoch,
) -> Option<token::Amount> {
let total_active = unwrap_client_response::<C, token::Amount>(
RPC.vp()
.pos()
.bond_with_slashing(client, delegator, validator, &Some(epoch))
.await,
);
Some(total_active)
}
pub async fn get_all_validators<C: Client + Sync>(
client: &C,
epoch: Epoch,
) -> HashSet<Address> {
namada_sdk::rpc::get_all_validators(client, epoch)
.await
.unwrap()
}
pub async fn get_total_staked_tokens<C: Client + Sync>(
client: &C,
epoch: Epoch,
) -> token::Amount {
namada_sdk::rpc::get_total_staked_tokens(client, epoch)
.await
.unwrap()
}
async fn get_validator_stake<C: Client + Sync>(
client: &C,
epoch: Epoch,
validator: &Address,
) -> Option<token::Amount> {
unwrap_client_response::<C, Option<token::Amount>>(
RPC.vp()
.pos()
.validator_stake(client, validator, &Some(epoch))
.await,
)
}
pub async fn get_delegation_validators<C: Client + Sync>(
client: &C,
address: &Address,
) -> HashSet<Address> {
let epoch = namada_sdk::rpc::query_epoch(client).await.unwrap();
namada_sdk::rpc::get_delegation_validators(client, address, epoch)
.await
.unwrap()
}
pub async fn get_delegations_of_delegator_at<C: Client + Sync>(
client: &C,
address: &Address,
epoch: Epoch,
) -> HashMap<Address, token::Amount> {
namada_sdk::rpc::get_delegations_of_delegator_at(client, address, epoch)
.await
.unwrap()
}
pub async fn query_governance_parameters<C: Client + Sync>(
client: &C,
) -> GovernanceParameters {
namada_sdk::rpc::query_governance_parameters(client).await
}
fn unwrap_client_response<C: Client, T>(response: Result<T, C::Error>) -> T {
response.unwrap_or_else(|err| {
eprintln!("Error in the query: {:?}", err);
cli::safe_exit(1)
})
}
fn unwrap_sdk_result<T>(response: Result<T, namada_sdk::error::Error>) -> T {
response.unwrap_or_else(|err| {
eprintln!("Error in the query: {:?}", err);
cli::safe_exit(1)
})
}
pub async fn compute_proposal_votes<C: Client + Sync>(
client: &C,
proposal_id: u64,
epoch: Epoch,
) -> ProposalVotes {
let votes = namada_sdk::rpc::query_proposal_votes(client, proposal_id)
.await
.unwrap();
let mut validators_vote: HashMap<Address, ProposalVote> =
HashMap::default();
let mut validator_voting_power: HashMap<Address, VotePower> =
HashMap::default();
let mut delegators_vote: HashMap<Address, ProposalVote> =
HashMap::default();
let mut delegator_voting_power: HashMap<
Address,
HashMap<Address, VotePower>,
> = HashMap::default();
for vote in votes {
if vote.is_validator() {
let validator_stake =
get_validator_stake(client, epoch, &vote.validator.clone())
.await
.unwrap_or_default();
validators_vote.insert(vote.validator.clone(), vote.data);
validator_voting_power.insert(vote.validator, validator_stake);
} else {
let delegator_stake = get_bond_amount_at(
client,
&vote.delegator,
&vote.validator,
epoch,
)
.await;
if let Some(stake) = delegator_stake {
delegators_vote.insert(vote.delegator.clone(), vote.data);
delegator_voting_power
.entry(vote.delegator.clone())
.or_default()
.insert(vote.validator, stake);
} else {
continue;
}
}
}
ProposalVotes {
validators_vote,
validator_voting_power,
delegators_vote,
delegator_voting_power,
}
}
pub async fn query_ibc_rate_limits(
context: &impl Namada,
args: args::QueryIbcRateLimit,
) {
let token_alias = lookup_token_alias(context, &args.token, None).await;
let native_token = context.native_token();
let IbcTokenRateLimits {
mint_limit,
throughput_per_epoch_limit,
} = unwrap_sdk_result(
rpc::query_ibc_rate_limits(context.client(), &args.token).await,
);
let (mint_str, throughput_str) = if args.token == native_token {
(
mint_limit.to_string_native(),
throughput_per_epoch_limit.to_string_native(),
)
} else {
(
mint_limit.to_string(),
throughput_per_epoch_limit.to_string(),
)
};
display_line!(
context.io(),
"IBC rate limits for token {}:\nGlobal mint limit: {}\nThroughput \
limit: {} per epoch",
token_alias,
mint_str,
throughput_str
);
}