diff --git a/.changelog/unreleased/improvements/1518-pos-bonds-query-reuse.md b/.changelog/unreleased/improvements/1518-pos-bonds-query-reuse.md new file mode 100644 index 00000000000..84f7a75e631 --- /dev/null +++ b/.changelog/unreleased/improvements/1518-pos-bonds-query-reuse.md @@ -0,0 +1,2 @@ +- PoS: make a re-usable bonds and unbonds details query. + ([\#1518](https://github.com/anoma/namada/pull/1518)) \ No newline at end of file diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 6eb99c07861..f68c7b2d97b 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -32,7 +32,9 @@ use namada::ledger::pos::{ self, BondId, BondsAndUnbondsDetail, CommissionPair, PosParams, Slash, }; use namada::ledger::queries::RPC; -use namada::ledger::rpc::{query_epoch, TxResponse}; +use namada::ledger::rpc::{ + enriched_bonds_and_unbonds, query_epoch, TxResponse, +}; use namada::ledger::storage::ConversionState; use namada::ledger::wallet::{AddressVpType, Wallet}; use namada::proof_of_stake::types::WeightedValidator; @@ -1264,7 +1266,7 @@ pub async fn query_bonds( _wallet: &mut Wallet, args: args::QueryBonds, ) -> std::io::Result<()> { - let _epoch = query_and_print_epoch(client).await; + let epoch = query_and_print_epoch(client).await; let source = args.owner; let validator = args.validator; @@ -1272,21 +1274,10 @@ pub async fn query_bonds( let stdout = io::stdout(); let mut w = stdout.lock(); - let bonds_and_unbonds: pos::types::BondsAndUnbondsDetails = - unwrap_client_response::( - RPC.vp() - .pos() - .bonds_and_unbonds(client, &source, &validator) - .await, - ); - let mut bonds_total: token::Amount = 0.into(); - let mut bonds_total_slashed: token::Amount = 0.into(); - let mut unbonds_total: token::Amount = 0.into(); - let mut unbonds_total_slashed: token::Amount = 0.into(); - let mut total_withdrawable: token::Amount = 0.into(); - for (bond_id, details) in bonds_and_unbonds { - let mut total: token::Amount = 0.into(); - let mut total_slashed: token::Amount = 0.into(); + let bonds_and_unbonds = + enriched_bonds_and_unbonds(client, epoch, &source, &validator).await; + + 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 { @@ -1296,74 +1287,66 @@ pub async fn query_bonds( ) }; writeln!(w, "{}:", bond_type)?; - for bond in details.bonds { + for bond in &details.data.bonds { writeln!( w, " Remaining active bond from epoch {}: Δ {}", bond.start, bond.amount )?; - total += bond.amount; - total_slashed += bond.slashed_amount.unwrap_or_default(); } - if total_slashed != token::Amount::default() { + if details.bonds_total_slashed != token::Amount::default() { writeln!( w, "Active (slashed) bonds total: {}", - total - total_slashed + details.bonds_total_active() )?; } - writeln!(w, "Bonds total: {}", total)?; + writeln!(w, "Bonds total: {}", details.bonds_total)?; writeln!(w)?; - bonds_total += total; - bonds_total_slashed += total_slashed; - let mut withdrawable = token::Amount::default(); - if !details.unbonds.is_empty() { - let mut total: token::Amount = 0.into(); - let mut total_slashed: token::Amount = 0.into(); + 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) }; writeln!(w, "{}:", bond_type)?; - for unbond in details.unbonds { - total += unbond.amount; - total_slashed += unbond.slashed_amount.unwrap_or_default(); + for unbond in &details.data.unbonds { writeln!( w, " Withdrawable from epoch {} (active from {}): Δ {}", unbond.withdraw, unbond.start, unbond.amount )?; } - withdrawable = total - total_slashed; - writeln!(w, "Unbonded total: {}", total)?; - - unbonds_total += total; - unbonds_total_slashed += total_slashed; - total_withdrawable += withdrawable; + writeln!(w, "Unbonded total: {}", details.unbonds_total)?; } - writeln!(w, "Withdrawable total: {}", withdrawable)?; + writeln!(w, "Withdrawable total: {}", details.total_withdrawable)?; writeln!(w)?; } - if bonds_total != bonds_total_slashed { + if bonds_and_unbonds.bonds_total != bonds_and_unbonds.bonds_total_slashed { writeln!( w, "All bonds total active: {}", - bonds_total - bonds_total_slashed + bonds_and_unbonds.bonds_total_active() )?; } - writeln!(w, "All bonds total: {}", bonds_total)?; + writeln!(w, "All bonds total: {}", bonds_and_unbonds.bonds_total)?; - if unbonds_total != unbonds_total_slashed { + if bonds_and_unbonds.unbonds_total + != bonds_and_unbonds.unbonds_total_slashed + { writeln!( w, "All unbonds total active: {}", - unbonds_total - unbonds_total_slashed + bonds_and_unbonds.unbonds_total_active() )?; } - writeln!(w, "All unbonds total: {}", unbonds_total)?; - writeln!(w, "All unbonds total withdrawable: {}", total_withdrawable)?; + writeln!(w, "All unbonds total: {}", bonds_and_unbonds.unbonds_total)?; + writeln!( + w, + "All unbonds total withdrawable: {}", + bonds_and_unbonds.total_withdrawable + )?; Ok(()) } diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index 145aa274a00..0f7c5769b91 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -9,9 +9,7 @@ pub use types::Client; pub use types::{ EncodedResponseQuery, RequestCtx, RequestQuery, ResponseQuery, Router, }; -use vp::VP; -// Re-export to show in rustdoc! -pub use vp::{Pos, Vp}; +use vp::{Vp, VP}; use super::storage::{DBIter, StorageHasher, DB}; use super::storage_api; @@ -22,7 +20,7 @@ use crate::types::storage::BlockHeight; mod router; mod shell; mod types; -mod vp; +pub mod vp; // Most commonly expected patterns should be declared first router! {RPC, diff --git a/shared/src/ledger/queries/vp/mod.rs b/shared/src/ledger/queries/vp/mod.rs index 5f5b5f60816..e575790bb4a 100644 --- a/shared/src/ledger/queries/vp/mod.rs +++ b/shared/src/ledger/queries/vp/mod.rs @@ -1,13 +1,16 @@ +//! Queries router and handlers for validity predicates + // Re-export to show in rustdoc! pub use pos::Pos; use pos::POS; -mod pos; +pub mod pos; // Validity predicate queries router! {VP, ( "pos" ) = (sub POS), } +/// Client-only methods for the router type are composed from router functions. #[cfg(any(test, feature = "async-client"))] pub mod client_only_methods { #[cfg(not(feature = "mainnet"))] diff --git a/shared/src/ledger/queries/vp/pos.rs b/shared/src/ledger/queries/vp/pos.rs index 81fc3f681b2..7e89e0293e7 100644 --- a/shared/src/ledger/queries/vp/pos.rs +++ b/shared/src/ledger/queries/vp/pos.rs @@ -1,9 +1,13 @@ +//! Queries router and handlers for PoS validity predicate + use std::collections::{HashMap, HashSet}; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use namada_core::ledger::storage_api::collections::lazy_map; use namada_core::ledger::storage_api::OptionExt; use namada_proof_of_stake::types::{ - BondId, BondsAndUnbondsDetails, CommissionPair, Slash, WeightedValidator, + BondId, BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionPair, + Slash, WeightedValidator, }; use namada_proof_of_stake::{ self, below_capacity_validator_set_handle, bond_amount, bond_handle, @@ -87,6 +91,46 @@ router! {POS, } +/// Enriched bonds data with extra information calculated from the data queried +/// from the node. +#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct Enriched { + /// The queried data + pub data: T, + /// Sum of the bond amounts + pub bonds_total: token::Amount, + /// Sum of the bond slashed amounts + pub bonds_total_slashed: token::Amount, + /// Sum of the unbond amounts + pub unbonds_total: token::Amount, + /// Sum of the unbond slashed amounts + pub unbonds_total_slashed: token::Amount, + /// Sum ofthe withdrawable amounts + pub total_withdrawable: token::Amount, +} + +/// Bonds and unbonds with all details (slashes and rewards, if any) grouped by +/// their bond IDs enriched with extra information calculated from the data +/// queried from the node. +pub type EnrichedBondsAndUnbondsDetails = + Enriched>; + +/// Bonds and unbonds with all details (slashes and rewards, if any) enriched +/// with extra information calculated from the data queried from the node. +pub type EnrichedBondsAndUnbondsDetail = Enriched; + +impl Enriched { + /// The bonds amount reduced by slashes + pub fn bonds_total_active(&self) -> token::Amount { + self.bonds_total - self.bonds_total_slashed + } + + /// The unbonds amount reduced by slashes + pub fn unbonds_total_active(&self) -> token::Amount { + self.unbonds_total - self.unbonds_total_slashed + } +} + // Handlers that implement the functions via `trait StorageRead`: /// Find if the given address belongs to a validator account. @@ -473,3 +517,97 @@ where { namada_proof_of_stake::find_validator_by_raw_hash(ctx.wl_storage, tm_addr) } + +/// Client-only methods for the router type are composed from router functions. +#[cfg(any(test, feature = "async-client"))] +pub mod client_only_methods { + use super::*; + use crate::ledger::queries::{Client, RPC}; + + impl Pos { + /// Get bonds and unbonds with all details (slashes and rewards, if any) + /// grouped by their bond IDs, enriched with extra information + /// calculated from the data. + pub async fn enriched_bonds_and_unbonds( + &self, + client: &CLIENT, + current_epoch: Epoch, + source: &Option
, + validator: &Option
, + ) -> Result::Error> + where + CLIENT: Client + Sync, + { + let data = RPC + .vp() + .pos() + .bonds_and_unbonds(client, source, validator) + .await?; + Ok(enrich_bonds_and_unbonds(current_epoch, data)) + } + } +} + +/// Calculate extra information from the bonds and unbonds details. +fn enrich_bonds_and_unbonds( + current_epoch: Epoch, + bonds_and_unbonds: BondsAndUnbondsDetails, +) -> EnrichedBondsAndUnbondsDetails { + let mut bonds_total: token::Amount = 0.into(); + let mut bonds_total_slashed: token::Amount = 0.into(); + let mut unbonds_total: token::Amount = 0.into(); + let mut unbonds_total_slashed: token::Amount = 0.into(); + let mut total_withdrawable: token::Amount = 0.into(); + + let enriched_details: HashMap = + bonds_and_unbonds + .into_iter() + .map(|(bond_id, detail)| { + let mut bond_total: token::Amount = 0.into(); + let mut bond_total_slashed: token::Amount = 0.into(); + let mut unbond_total: token::Amount = 0.into(); + let mut unbond_total_slashed: token::Amount = 0.into(); + let mut withdrawable: token::Amount = 0.into(); + + for bond in &detail.bonds { + bond_total += bond.amount; + bond_total_slashed += + bond.slashed_amount.unwrap_or_default(); + } + for unbond in &detail.unbonds { + unbond_total += unbond.amount; + unbond_total_slashed += + unbond.slashed_amount.unwrap_or_default(); + + if current_epoch >= unbond.withdraw { + withdrawable += unbond.amount + - unbond.slashed_amount.unwrap_or_default() + } + } + + bonds_total += bond_total; + bonds_total_slashed += bond_total_slashed; + unbonds_total += unbond_total; + unbonds_total_slashed += unbond_total_slashed; + total_withdrawable += withdrawable; + + let enriched_detail = EnrichedBondsAndUnbondsDetail { + data: detail, + bonds_total: bond_total, + bonds_total_slashed: bond_total_slashed, + unbonds_total: unbond_total, + unbonds_total_slashed: unbond_total_slashed, + total_withdrawable: withdrawable, + }; + (bond_id, enriched_detail) + }) + .collect(); + EnrichedBondsAndUnbondsDetails { + data: enriched_details, + bonds_total, + bonds_total_slashed, + unbonds_total, + unbonds_total_slashed, + total_withdrawable, + } +} diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index b3db4d09ec0..a108de00fdb 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -10,7 +10,7 @@ use namada_core::ledger::testnet_pow; use namada_core::types::address::Address; use namada_core::types::storage::Key; use namada_core::types::token::Amount; -use namada_proof_of_stake::types::CommissionPair; +use namada_proof_of_stake::types::{BondsAndUnbondsDetails, CommissionPair}; use serde::Serialize; use tokio::time::Duration; @@ -18,6 +18,7 @@ use crate::ledger::events::Event; use crate::ledger::governance::parameters::GovParams; use crate::ledger::governance::storage as gov_storage; use crate::ledger::native_vp::governance::utils::Votes; +use crate::ledger::queries::vp::pos::EnrichedBondsAndUnbondsDetails; use crate::ledger::queries::RPC; use crate::proto::Tx; use crate::tendermint::merkle::proof::Proof; @@ -881,3 +882,42 @@ pub async fn get_bond_amount_at( ); Some(total_active) } + +/// Get bonds and unbonds with all details (slashes and rewards, if any) +/// grouped by their bond IDs. +pub async fn bonds_and_unbonds( + client: &C, + source: &Option
, + validator: &Option
, +) -> BondsAndUnbondsDetails { + unwrap_client_response::( + RPC.vp() + .pos() + .bonds_and_unbonds(client, source, validator) + .await, + ) +} + +/// Get bonds and unbonds with all details (slashes and rewards, if any) +/// grouped by their bond IDs, enriched with extra information calculated from +/// the data. +pub async fn enriched_bonds_and_unbonds< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + current_epoch: Epoch, + source: &Option
, + validator: &Option
, +) -> EnrichedBondsAndUnbondsDetails { + unwrap_client_response::( + RPC.vp() + .pos() + .enriched_bonds_and_unbonds( + client, + current_epoch, + source, + validator, + ) + .await, + ) +}