diff --git a/.changelog/unreleased/improvements/3691-cli-commands.md b/.changelog/unreleased/improvements/3691-cli-commands.md new file mode 100644 index 0000000000..50d93a4961 --- /dev/null +++ b/.changelog/unreleased/improvements/3691-cli-commands.md @@ -0,0 +1,3 @@ +- Include some CLI commands for querying the total supply of any token + and the effective total circulating supply of the native token. + ([\#3691](https://github.com/anoma/namada/pull/3691)) \ No newline at end of file diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index c1b4c68534..6781374b97 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -295,6 +295,8 @@ pub mod cmds { .subcommand(QueryCommissionRate::def().display_order(5)) .subcommand(QueryRewards::def().display_order(5)) .subcommand(QueryMetaData::def().display_order(5)) + .subcommand(QueryTotalSupply::def().display_order(5)) + .subcommand(QueryEffNativeSupply::def().display_order(5)) // Actions .subcommand(SignTx::def().display_order(6)) .subcommand(ShieldedSync::def().display_order(6)) @@ -366,6 +368,10 @@ pub mod cmds { let query_rewards = Self::parse_with_ctx(matches, QueryRewards); let query_delegations = Self::parse_with_ctx(matches, QueryDelegations); + let query_total_supply = + Self::parse_with_ctx(matches, QueryTotalSupply); + let query_native_supply = + Self::parse_with_ctx(matches, QueryEffNativeSupply); let query_find_validator = Self::parse_with_ctx(matches, QueryFindValidator); let query_result = Self::parse_with_ctx(matches, QueryResult); @@ -440,6 +446,8 @@ pub mod cmds { .or(query_validator_state) .or(query_commission) .or(query_metadata) + .or(query_total_supply) + .or(query_native_supply) .or(query_account) .or(sign_tx) .or(shielded_sync) @@ -523,6 +531,8 @@ pub mod cmds { QueryMetaData(QueryMetaData), QuerySlashes(QuerySlashes), QueryDelegations(QueryDelegations), + QueryTotalSupply(QueryTotalSupply), + QueryEffNativeSupply(QueryEffNativeSupply), QueryFindValidator(QueryFindValidator), QueryRawBytes(QueryRawBytes), QueryProposal(QueryProposal), @@ -2052,6 +2062,61 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct QueryTotalSupply(pub args::QueryTotalSupply); + + impl SubCmd for QueryTotalSupply { + const CMD: &'static str = "total-supply"; + + fn parse(matches: &ArgMatches) -> Option + where + Self: Sized, + { + matches.subcommand_matches(Self::CMD).map(|matches| { + QueryTotalSupply(args::QueryTotalSupply::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about(wrap!( + "Query the total supply in the network of the given \ + token. For the native token, this will query the raw \ + total supply and not the effective total supply." + )) + .add_args::>() + } + } + + #[derive(Clone, Debug)] + pub struct QueryEffNativeSupply( + pub args::QueryEffNativeSupply, + ); + + impl SubCmd for QueryEffNativeSupply { + const CMD: &'static str = "native-supply"; + + fn parse(matches: &ArgMatches) -> Option + where + Self: Sized, + { + matches.subcommand_matches(Self::CMD).map(|matches| { + QueryEffNativeSupply(args::QueryEffNativeSupply::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about(wrap!( + "Query the effective total circulating supply of the \ + native token NAM. This excludes illquid NAM tokens held \ + in places such as the PGF account. This is the token \ + amount used in inflation calculations." + )) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct QueryFindValidator(pub args::QueryFindValidator); @@ -6957,6 +7022,59 @@ pub mod args { } } + impl Args for QueryTotalSupply { + fn parse(matches: &ArgMatches) -> Self { + let query = Query::parse(matches); + let token = TOKEN.parse(matches); + Self { query, token } + } + + fn def(app: App) -> App { + app.add_args::>() + .arg(TOKEN.def().help(wrap!("The token address."))) + } + } + + impl CliToSdk> for QueryTotalSupply { + type Error = std::convert::Infallible; + + fn to_sdk( + self, + ctx: &mut Context, + ) -> Result, Self::Error> { + Ok(QueryTotalSupply:: { + query: self.query.to_sdk(ctx)?, + token: ctx.borrow_chain_or_exit().get(&self.token), + }) + } + } + + impl Args for QueryEffNativeSupply { + fn parse(matches: &ArgMatches) -> Self { + let query = Query::parse(matches); + Self { query } + } + + fn def(app: App) -> App { + app.add_args::>() + } + } + + impl CliToSdk> + for QueryEffNativeSupply + { + type Error = std::convert::Infallible; + + fn to_sdk( + self, + ctx: &mut Context, + ) -> Result, Self::Error> { + Ok(QueryEffNativeSupply:: { + query: self.query.to_sdk(ctx)?, + }) + } + } + impl Args for QueryFindValidator { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index 1d9ce01203..6724f24a73 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -637,6 +637,29 @@ impl CliApi { let namada = ctx.to_sdk(client, io); rpc::query_delegations(&namada, args).await; } + Sub::QueryTotalSupply(QueryTotalSupply(args)) => { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let ledger_address = + chain_ctx.get(&args.query.ledger_address); + let client = client.unwrap_or_else(|| { + C::from_tendermint_address(&ledger_address) + }); + client.wait_until_node_is_synced(&io).await?; + let args = args.to_sdk(&mut ctx)?; + let namada = ctx.to_sdk(client, io); + rpc::query_total_supply(&namada, args).await; + } + Sub::QueryEffNativeSupply(QueryEffNativeSupply(args)) => { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let ledger_address = + chain_ctx.get(&args.query.ledger_address); + let client = client.unwrap_or_else(|| { + C::from_tendermint_address(&ledger_address) + }); + client.wait_until_node_is_synced(&io).await?; + let namada = ctx.to_sdk(client, io); + rpc::query_effective_native_supply(&namada).await; + } Sub::QueryFindValidator(QueryFindValidator(args)) => { let chain_ctx = ctx.borrow_mut_chain_or_exit(); let ledger_address = diff --git a/crates/apps_lib/src/client/rpc.rs b/crates/apps_lib/src/client/rpc.rs index ec48adc9ba..a642bd4856 100644 --- a/crates/apps_lib/src/client/rpc.rs +++ b/crates/apps_lib/src/client/rpc.rs @@ -36,7 +36,8 @@ use namada_sdk::proof_of_stake::types::{ use namada_sdk::proof_of_stake::PosParams; use namada_sdk::queries::{Client, RPC}; use namada_sdk::rpc::{ - self, enriched_bonds_and_unbonds, query_epoch, TxResponse, + self, enriched_bonds_and_unbonds, format_denominated_amount, query_epoch, + TxResponse, }; use namada_sdk::storage::BlockResults; use namada_sdk::tendermint_rpc::endpoint::status; @@ -1176,6 +1177,40 @@ pub async fn query_rewards( ) } +/// Query token total supply. +pub async fn query_total_supply( + 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 + ); +} + +/// Query the effective total supply of the native token +pub async fn query_effective_native_supply(context: &N) { + let native_supply = unwrap_client_response::( + RPC.vp() + .token() + .effective_native_supply(context.client()) + .await, + ); + display_line!(context.io(), "nam: {}", native_supply.to_string_native()); +} + /// Query a validator's state information pub async fn query_and_print_validator_state( context: &impl Namada, @@ -2008,6 +2043,14 @@ fn unwrap_client_response( }) } +/// A helper to unwrap an SDK query result. Will shut down process on error. +fn unwrap_sdk_result(response: Result) -> T { + response.unwrap_or_else(|err| { + eprintln!("Error in the query: {:?}", err); + cli::safe_exit(1) + }) +} + pub async fn compute_proposal_votes( client: &C, proposal_id: u64, diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index 00bf1675d8..5949a16ba4 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -2220,6 +2220,22 @@ pub struct QueryDelegations { pub owner: C::Address, } +/// Query token total supply +#[derive(Clone, Debug)] +pub struct QueryTotalSupply { + /// Common query args + pub query: Query, + /// Token address + pub token: C::Address, +} + +/// Query effective native supply +#[derive(Clone, Debug)] +pub struct QueryEffNativeSupply { + /// Common query args + pub query: Query, +} + /// Query PoS to find a validator #[derive(Clone, Debug)] pub struct QueryFindValidator { diff --git a/crates/sdk/src/queries/vp/token.rs b/crates/sdk/src/queries/vp/token.rs index 7bae2619a0..3904f1cd85 100644 --- a/crates/sdk/src/queries/vp/token.rs +++ b/crates/sdk/src/queries/vp/token.rs @@ -10,38 +10,45 @@ use namada_token::{ use crate::queries::RequestCtx; router! {TOKEN, - ( "denomination" / [addr: Address] ) -> Option = denomination, - ( "total_supply" / [addr: Address] ) -> token::Amount = total_supply, + ( "denomination" / [token: Address] ) -> Option = denomination, + ( "total_supply" / [token: Address] ) -> token::Amount = total_supply, + ( "effective_native_supply" ) -> token::Amount = effective_native_supply, } /// Get the number of decimal places (in base 10) for a /// token specified by `addr`. fn denomination( ctx: RequestCtx<'_, D, H, V, T>, - addr: Address, + token: Address, ) -> namada_storage::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - read_denom(ctx.state, &addr) + read_denom(ctx.state, &token) } /// Get the total supply for a token address fn total_supply( ctx: RequestCtx<'_, D, H, V, T>, - addr: Address, + token: Address, ) -> namada_storage::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let native_token = ctx.state.in_mem().native_token.clone(); - if addr == native_token { - get_effective_total_native_supply(ctx.state) - } else { - read_total_supply(ctx.state, &addr) - } + read_total_supply(ctx.state, &token) +} + +/// Get the effective total supply of the native token +fn effective_native_supply( + ctx: RequestCtx<'_, D, H, V, T>, +) -> namada_storage::Result +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + get_effective_total_native_supply(ctx.state) } pub mod client_only_methods { diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 44a2ab554a..96e82e7686 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -224,6 +224,15 @@ pub async fn get_token_total_supply( convert_response::(RPC.vp().token().total_supply(client, token).await) } +/// Query the effective total supply of the native token +pub async fn get_effective_native_supply( + client: &C, +) -> Result { + convert_response::( + RPC.vp().token().effective_native_supply(client).await, + ) +} + /// Check if the given address is a known validator. pub async fn is_validator( client: &C, diff --git a/crates/tests/src/e2e/setup.rs b/crates/tests/src/e2e/setup.rs index b2651d6a0d..256d980853 100644 --- a/crates/tests/src/e2e/setup.rs +++ b/crates/tests/src/e2e/setup.rs @@ -1458,6 +1458,7 @@ pub mod constants { // Native VP aliases pub const GOVERNANCE_ADDRESS: &str = "governance"; pub const MASP: &str = "masp"; + pub const PGF_ADDRESS: &str = "pgf"; // Fungible token addresses pub const NAM: &str = "NAM"; diff --git a/crates/tests/src/integration/ledger_tests.rs b/crates/tests/src/integration/ledger_tests.rs index fdd8d7233c..65c0862061 100644 --- a/crates/tests/src/integration/ledger_tests.rs +++ b/crates/tests/src/integration/ledger_tests.rs @@ -32,7 +32,8 @@ use test_log::test; use crate::e2e::ledger_tests::prepare_proposal_data; use crate::e2e::setup::constants::{ ALBERT, ALBERT_KEY, APFEL, BERTHA, BERTHA_KEY, BTC, CHRISTEL, CHRISTEL_KEY, - DAEWON, DOT, ESTER, ETH, GOVERNANCE_ADDRESS, KARTOFFEL, NAM, SCHNITZEL, + DAEWON, DOT, ESTER, ETH, GOVERNANCE_ADDRESS, KARTOFFEL, NAM, PGF_ADDRESS, + SCHNITZEL, }; use crate::e2e::setup::{apply_use_device, ensure_hot_key}; use crate::integration::helpers::{ @@ -1155,6 +1156,43 @@ fn pgf_governance_proposal() -> Result<()> { ); assert!(captured.contains("Pgf fundings: no fundings are currently set.")); + // 7.1 Query total NAM supply and PGF balance + let query_balance_args = vec![ + "balance", + "--owner", + PGF_ADDRESS, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + let captured = + CapturedOutput::of(|| run(&node, Bin::Client, query_balance_args)); + assert_matches!(captured.result, Ok(_)); + assert!(captured.contains("nam: 13.785266")); + + let query_total_supply_args = vec![ + "total-supply", + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + let captured = + CapturedOutput::of(|| run(&node, Bin::Client, query_total_supply_args)); + assert_matches!(captured.result, Ok(_)); + assert!(captured.contains( + "token tnam1q9kn74xfzytqkqyycfrhycr8ajam8ny935cge0z5: 114400023.904507" + )); + + let query_native_supply_args = + vec!["native-supply", "--ledger-address", &validator_one_rpc]; + let captured = CapturedOutput::of(|| { + run(&node, Bin::Client, query_native_supply_args) + }); + assert_matches!(captured.result, Ok(_)); + assert!(captured.contains("nam: 114400010.119241")); + // 8. Submit proposal funding let albert = defaults::albert_address(); let bertha = defaults::bertha_address();