diff --git a/.changelog/unreleased/improvements/3171-rework-balance.md b/.changelog/unreleased/improvements/3171-rework-balance.md new file mode 100644 index 0000000000..4b53271f4e --- /dev/null +++ b/.changelog/unreleased/improvements/3171-rework-balance.md @@ -0,0 +1,3 @@ +- Remove unbounded `token` and `owner` balance queries from the CLI, in + an attempt to reduce strain on the RPC servers of full/validator nodes. + ([\#3171](https://github.com/anoma/namada/pull/3171)) \ No newline at end of file diff --git a/crates/apps/src/lib/cli.rs b/crates/apps/src/lib/cli.rs index 0b60c01c12..d0b7d3544b 100644 --- a/crates/apps/src/lib/cli.rs +++ b/crates/apps/src/lib/cli.rs @@ -250,7 +250,6 @@ pub mod cmds { .subcommand(QueryMaspRewardTokens::def().display_order(5)) .subcommand(QueryBlock::def().display_order(5)) .subcommand(QueryBalance::def().display_order(5)) - .subcommand(QueryIbcToken::def().display_order(5)) .subcommand(QueryBonds::def().display_order(5)) .subcommand(QueryBondedStake::def().display_order(5)) .subcommand(QuerySlashes::def().display_order(5)) @@ -323,7 +322,6 @@ pub mod cmds { Self::parse_with_ctx(matches, QueryMaspRewardTokens); let query_block = Self::parse_with_ctx(matches, QueryBlock); let query_balance = Self::parse_with_ctx(matches, QueryBalance); - let query_ibc_token = Self::parse_with_ctx(matches, QueryIbcToken); let query_bonds = Self::parse_with_ctx(matches, QueryBonds); let query_bonded_stake = Self::parse_with_ctx(matches, QueryBondedStake); @@ -384,7 +382,6 @@ pub mod cmds { .or(query_masp_reward_tokens) .or(query_block) .or(query_balance) - .or(query_ibc_token) .or(query_bonds) .or(query_bonded_stake) .or(query_slashes) @@ -474,7 +471,6 @@ pub mod cmds { QueryMaspRewardTokens(QueryMaspRewardTokens), QueryBlock(QueryBlock), QueryBalance(QueryBalance), - QueryIbcToken(QueryIbcToken), QueryBonds(QueryBonds), QueryBondedStake(QueryBondedStake), QueryCommissionRate(QueryCommissionRate), @@ -1713,30 +1709,11 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) - .about("Query balance(s) of tokens.") + .about("Query the token balance of some account.") .add_args::>() } } - #[derive(Clone, Debug)] - pub struct QueryIbcToken(pub args::QueryIbcToken); - - impl SubCmd for QueryIbcToken { - const CMD: &'static str = "ibc-token"; - - fn parse(matches: &ArgMatches) -> Option { - matches.subcommand_matches(Self::CMD).map(|matches| { - QueryIbcToken(args::QueryIbcToken::parse(matches)) - }) - } - - fn def() -> App { - App::new(Self::CMD) - .about("Query IBC token(s).") - .add_args::>() - } - } - #[derive(Clone, Debug)] pub struct QueryBonds(pub args::QueryBonds); @@ -3026,7 +3003,7 @@ pub mod args { pub const AMOUNT: Arg = arg("amount"); pub const ARCHIVE_DIR: ArgOpt = arg_opt("archive-dir"); pub const AVATAR_OPT: ArgOpt = arg_opt("avatar"); - pub const BALANCE_OWNER: ArgOpt = arg_opt("owner"); + pub const BALANCE_OWNER: Arg = arg("owner"); pub const BASE_DIR: ArgDefault = arg_default( "base-dir", DefaultFn(|| match env::var("NAMADA_BASE_DIR") { @@ -5472,10 +5449,9 @@ pub mod args { Ok(QueryBalance:: { query, - owner: self.owner.map(|x| chain_ctx.get_cached(&x)), - token: self.token.map(|x| chain_ctx.get(&x)), + owner: chain_ctx.get_cached(&self.owner), + token: chain_ctx.get(&self.token), no_conversions: self.no_conversions, - show_ibc_tokens: self.show_ibc_tokens, }) } } @@ -5484,15 +5460,13 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let owner = BALANCE_OWNER.parse(matches); - let token = TOKEN_OPT.parse(matches); + let token = TOKEN.parse(matches); let no_conversions = NO_CONVERSIONS.parse(matches); - let show_ibc_tokens = SHOW_IBC_TOKENS.parse(matches); Self { query, owner, token, no_conversions, - show_ibc_tokens, } } @@ -5504,7 +5478,7 @@ pub mod args { .help("The account address whose balance to query."), ) .arg( - TOKEN_OPT + TOKEN .def() .help("The token's address whose balance to query."), ) @@ -5513,50 +5487,6 @@ pub mod args { "Whether not to automatically perform conversions.", ), ) - .arg(SHOW_IBC_TOKENS.def().help( - "Show IBC tokens. When the given token is an IBC denom, \ - IBC tokens will be shown even if this flag is false.", - )) - } - } - - impl CliToSdk> for QueryIbcToken { - type Error = std::convert::Infallible; - - fn to_sdk( - self, - ctx: &mut Context, - ) -> Result, Self::Error> { - let query = self.query.to_sdk(ctx)?; - let chain_ctx = ctx.borrow_mut_chain_or_exit(); - Ok(QueryIbcToken:: { - query, - token: self.token, - owner: self.owner.map(|x| chain_ctx.get_cached(&x)), - }) - } - } - - impl Args for QueryIbcToken { - fn parse(matches: &ArgMatches) -> Self { - let query = Query::parse(matches); - let token = TOKEN_STR_OPT.parse(matches); - let owner = BALANCE_OWNER.parse(matches); - Self { - query, - owner, - token, - } - } - - fn def(app: App) -> App { - app.add_args::>() - .arg(TOKEN_STR_OPT.def().help("The base token to query.")) - .arg( - BALANCE_OWNER - .def() - .help("The account address whose token to query."), - ) } } diff --git a/crates/apps/src/lib/cli/client.rs b/crates/apps/src/lib/cli/client.rs index d937283c95..da92be0326 100644 --- a/crates/apps/src/lib/cli/client.rs +++ b/crates/apps/src/lib/cli/client.rs @@ -511,18 +511,6 @@ impl CliApi { let namada = ctx.to_sdk(client, io); rpc::query_balance(&namada, args).await; } - Sub::QueryIbcToken(QueryIbcToken(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_ibc_tokens(&namada, args).await; - } Sub::QueryBonds(QueryBonds(args)) => { let chain_ctx = ctx.borrow_mut_chain_or_exit(); let ledger_address = diff --git a/crates/apps/src/lib/cli/context.rs b/crates/apps/src/lib/cli/context.rs index 709ebdb05b..3037cf248c 100644 --- a/crates/apps/src/lib/cli/context.rs +++ b/crates/apps/src/lib/cli/context.rs @@ -621,5 +621,11 @@ impl ArgFromMutContext for BalanceOwner { ExtendedViewingKey::arg_from_mut_ctx(ctx, raw) .map(Self::FullViewingKey) }) + .map_err(|_| { + format!( + "Could not find {raw} in the wallet, nor parse it as a \ + transparent address or as a MASP viewing key" + ) + }) } } diff --git a/crates/apps/src/lib/client/rpc.rs b/crates/apps/src/lib/client/rpc.rs index 3127c46785..72dcf67def 100644 --- a/crates/apps/src/lib/client/rpc.rs +++ b/crates/apps/src/lib/client/rpc.rs @@ -10,12 +10,12 @@ 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::address::{Address, InternalAddress, MASP, MULTITOKEN}; +use namada::core::address::{Address, InternalAddress, MASP}; use namada::core::collections::{HashMap, HashSet}; use namada::core::hash::Hash; use namada::core::key::*; use namada::core::masp::BalanceOwner; -use namada::core::storage::{BlockHeight, BlockResults, Epoch, Key, KeySeg}; +use namada::core::storage::{BlockHeight, BlockResults, Epoch}; use namada::core::token::MaspDigitPos; use namada::governance::parameters::GovernanceParameters; use namada::governance::pgf::parameters::PgfParameters; @@ -28,7 +28,6 @@ use namada::governance::utils::{ProposalVotes, VotePower}; use namada::governance::ProposalVote; use namada::io::Io; use namada::ledger::events::Event; -use namada::ledger::ibc::storage::ibc_trace_key; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; use namada::ledger::pos::types::{CommissionPair, Slash}; use namada::ledger::pos::PosParams; @@ -38,7 +37,6 @@ use namada::proof_of_stake::types::{ }; use namada::{state as storage, token}; use namada_sdk::control_flow::time::{Duration, Instant}; -use namada_sdk::error::QueryError; use namada_sdk::masp::MaspTokenRewardData; use namada_sdk::proof_of_stake::types::ValidatorMetaData; use namada_sdk::queries::Client; @@ -181,312 +179,75 @@ pub async fn query_raw_bytes( /// Query token balance(s) pub async fn query_balance(context: &impl Namada, args: args::QueryBalance) { - // Query the balances of shielded or transparent account types depending on - // the CLI arguments match &args.owner { - Some(BalanceOwner::FullViewingKey(_viewing_key)) => { - query_shielded_balance(context, args).await - } - Some(BalanceOwner::Address(_owner)) => { + BalanceOwner::Address(_) => { query_transparent_balance(context, args).await } - None => { - // Print shielded balance - query_shielded_balance(context, args.clone()).await; - // Then print transparent balance - query_transparent_balance(context, args).await; + BalanceOwner::FullViewingKey(_) => { + query_shielded_balance(context, args).await } - }; + } } /// Query token balance(s) -pub async fn query_transparent_balance( +async fn query_transparent_balance( context: &impl Namada, args: args::QueryBalance, ) { - match (args.token, args.owner) { - (Some(base_token), Some(owner)) => { - let owner = owner.address().unwrap(); - let tokens = query_tokens( - context, - Some(&base_token), - Some(&owner), - args.show_ibc_tokens, - ) - .await; - for (token_alias, token) in tokens { - let balance_key = - token::storage_key::balance_key(&token, &owner); - match query_storage_value::<_, token::Amount>( - context.client(), - &balance_key, - ) - .await - { - Ok(balance) => { - let balance = - context.format_amount(&token, balance).await; - display_line!( - context.io(), - "{}: {}", - token_alias, - balance - ); - } - Err(namada_sdk::error::Error::Query( - QueryError::NoSuchKey(_), - )) => { - display_line!( - context.io(), - "No {token_alias} balance found for {owner}", - ) - } - Err(e) => { - display_line!( - context.io(), - "Error querying balance of {token_alias}: {e}" - ); - } - } - } - } - (None, Some(owner)) => { - let owner = owner.address().unwrap(); - let tokens = - query_tokens(context, None, Some(&owner), args.show_ibc_tokens) - .await; - for (token_alias, token) in tokens { - let balance = - get_token_balance(context.client(), &token, &owner).await; - if !balance.is_zero() { - let balance = context.format_amount(&token, balance).await; - display_line!(context.io(), "{}: {}", token_alias, balance); - } - } - } - (Some(base_token), None) => { - let tokens = query_tokens( - context, - Some(&base_token), - None, - args.show_ibc_tokens, - ) + let args::QueryBalance { + // Token owner (needs to be a transparent address) + owner, + // The token to query + token, + .. + } = args; + + let owner = owner + .address() + .expect("Balance owner should have been a transparent address"); + + let token_alias = lookup_token_alias(context, &token, &owner).await; + let token_balance_result = + namada_sdk::rpc::get_token_balance(context.client(), &token, &owner) .await; - for (_, token) in tokens { - let prefix = token::storage_key::balance_prefix(&token); - let balances = - query_storage_prefix::(context, &prefix) - .await; - if let Some(balances) = balances { - print_balances(context, balances, Some(&token), None).await; - } - } - } - (None, None) => { - let prefix = Key::from(MULTITOKEN.to_db_key()); - let balances = query_storage_prefix(context, &prefix).await; - if let Some(balances) = balances { - print_balances(context, balances, None, None).await; - } - } - } -} -async fn print_balances( - context: &impl Namada, - balances: impl Iterator, - token: Option<&Address>, - target: Option<&Address>, -) { - let stdout = io::stdout(); - let mut w = stdout.lock(); - let wallet = context.wallet().await; - - let mut print_num = 0; - let mut print_token = None; - for (key, balance) in balances { - // Get the token, the owner, and the balance with the token and the - // owner - let (t, o, s) = match token::storage_key::is_any_token_balance_key(&key) - { - Some([tok, owner]) => ( - tok.clone(), - owner.clone(), - format!( - ": {}, owned by {}", - context.format_amount(tok, balance).await, - wallet.lookup_alias(owner) - ), - ), - None => continue, - }; - let token_alias = lookup_token_alias(context, &t, &o).await; - // Get the token and the balance - let (t, s) = match (token, target) { - // the given token and the given target are the same as the - // retrieved ones - (Some(token), Some(target)) if t == *token && o == *target => { - (t, s) - } - // the given token is the same as the retrieved one - (Some(token), None) if t == *token => (t, s), - // the given target is the same as the retrieved one - (None, Some(target)) if o == *target => (t, s), - // no specified token or target - (None, None) => (t, s), - // otherwise, this balance will not be printed - _ => continue, - }; - // Print the token if it isn't printed yet - match &print_token { - Some(token) if *token == t => { - // the token has been already printed - } - _ => { - display_line!(context.io(), &mut w; "Token {}", token_alias) - .unwrap(); - print_token = Some(t); - } + match token_balance_result { + Ok(balance) => { + let balance = context.format_amount(&token, balance).await; + display_line!(context.io(), "{token_alias}: {balance}"); } - // Print the balance - display_line!(context.io(), &mut w; "{}", s).unwrap(); - print_num += 1; - } - - if print_num == 0 { - match (token, target) { - (Some(_), Some(target)) | (None, Some(target)) => display_line!( + Err(e) => { + display_line!( context.io(), - &mut w; - "No balances owned by {}", - wallet.lookup_alias(target) - ) - .unwrap(), - (Some(token), None) => { - let token_alias = wallet.lookup_alias(token); - display_line!(context.io(), &mut w; "No balances for token {}", token_alias).unwrap() - } - (None, None) => { - display_line!(context.io(), &mut w; "No balances").unwrap() - } + "Error querying balance of {token_alias}: {e}" + ); } } } +/// Return the token alias of the given `token`. async fn lookup_token_alias( context: &impl Namada, token: &Address, owner: &Address, ) -> String { match token { - Address::Internal(InternalAddress::IbcToken(trace_hash)) => { - let ibc_trace_key = - ibc_trace_key(owner.to_string(), trace_hash.to_string()); - match query_storage_value::<_, String>( - context.client(), - &ibc_trace_key, - ) - .await - { - Ok(ibc_trace) => { - context.wallet().await.lookup_ibc_token_alias(ibc_trace) - } - Err(_) => token.to_string(), - } - } Address::Internal(InternalAddress::Erc20(eth_addr)) => { eth_addr.to_canonical() } - _ => context.wallet().await.lookup_alias(token), - } -} - -/// Returns pairs of token alias and token address -async fn query_tokens( - context: &impl Namada, - base_token: Option<&Address>, - owner: Option<&Address>, - show_ibc_tokens: bool, -) -> BTreeMap { - let wallet = context.wallet().await; - let mut tokens = BTreeMap::new(); - match base_token { - Some(token) - if matches!( - token, - Address::Internal(InternalAddress::IbcToken(_)) - ) => - { + Address::Internal(InternalAddress::IbcToken(_)) => { let ibc_denom = - rpc::query_ibc_denom(context, token.to_string(), owner).await; - let alias = - context.wallet().await.lookup_ibc_token_alias(ibc_denom); - tokens.insert(alias, token.clone()); - // we don't need to check other IBC prefixes - return tokens; - } - Some(token) => { - if let Address::Internal(InternalAddress::Erc20(eth_addr)) = token { - tokens.insert(eth_addr.to_string(), token.clone()); - } else { - let alias = wallet - .find_alias(token) - .map(|alias| alias.to_string()) - .unwrap_or(token.to_string()); - tokens.insert(alias, token.clone()); - } - } - None => tokens = wallet.tokens_with_aliases(), - } - - if !show_ibc_tokens { - return tokens; - } - - match rpc::query_ibc_tokens( - context, - base_token.map(|t| t.to_string()), - owner, - ) - .await - { - Ok(ibc_tokens) => { - for (trace, addr) in ibc_tokens { - let ibc_trace_alias = - context.wallet().await.lookup_ibc_token_alias(trace); - tokens.insert(ibc_trace_alias, addr); - } - } - Err(e) => { - edisplay_line!(context.io(), "IBC token query failed: {}", e); - } - } - tokens -} + rpc::query_ibc_denom(context, token.to_string(), Some(owner)) + .await; -pub async fn query_ibc_tokens( - context: &impl Namada, - args: args::QueryIbcToken, -) { - let wallet = context.wallet().await; - let token = args.token.map(|t| { - wallet - .find_address(&t) - .map(|addr| addr.to_string()) - .unwrap_or(t) - }); - let owner = args.owner.map(|o| o.address().unwrap_or(MASP)); - match rpc::query_ibc_tokens(context, token, owner.as_ref()).await { - Ok(ibc_tokens) => { - for (trace, addr) in ibc_tokens { - let alias = - context.wallet().await.lookup_ibc_token_alias(trace); - display_line!(context.io(), "{}: {}", alias, addr); - } - } - Err(e) => { - edisplay_line!(context.io(), "IBC token query failed: {}", e); + 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()), } } @@ -577,362 +338,95 @@ pub async fn query_proposal_by_id( } /// Query token shielded balance(s) -pub async fn query_shielded_balance( +async fn query_shielded_balance( context: &impl Namada, args: args::QueryBalance, ) { - // Used to control whether balances for all keys or a specific key are - // printed - let owner = args.owner.and_then(|x| x.full_viewing_key()); - // Used to control whether conversions are automatically performed - let no_conversions = args.no_conversions; - // Viewing keys are used to query shielded balances. If a spending key is - // provided, then convert to a viewing key first. - let viewing_keys = match owner { - Some(viewing_key) => vec![viewing_key], - None => context - .wallet() - .await - .get_viewing_keys() - .values() - .copied() - .collect(), - }; + let args::QueryBalance { + // Token owner (needs to be a viewing key) + owner, + // The token to query + token, + // Used to control whether conversions are automatically performed + 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; + + // Pre-compute the masp asset types of `token` { let mut shielded = context.shielded_mut().await; let _ = shielded.load().await; - // Precompute asset types to increase chances of success in decoding - let token_map = query_tokens( - context, - args.token.as_ref(), - None, - args.show_ibc_tokens, - ) - .await; - let tokens = token_map.values().collect(); let _ = shielded - .precompute_asset_types(context.client(), tokens) + .precompute_asset_types(context.client(), vec![&token]) .await; // Save the update state so that future fetches can be short-circuited let _ = shielded.save().await; } + // The epoch is required to identify timestamped tokens let epoch = query_and_print_epoch(context).await; - // Map addresses to token names - match (args.token, owner.is_some()) { - // Here the user wants to know the balance for a specific token - (Some(base_token), true) => { - let tokens = query_tokens( - context, - Some(&base_token), - Some(&MASP), - args.show_ibc_tokens, - ) - .await; - for (token_alias, token) in tokens { - // Query the multi-asset balance at the given spending key - let viewing_key = - ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; - let mut shielded = context.shielded_mut().await; - let balance = if no_conversions { - shielded - .compute_shielded_balance(&viewing_key) - .await - .unwrap() - .expect("context should contain viewing key") - } else { - shielded - .compute_exchanged_balance( - context.client(), - context.io(), - &viewing_key, - epoch, - ) - .await - .unwrap() - .expect("context should contain viewing key") - }; - - let total_balance = shielded - .decode_combine_sum_to_epoch( - context.client(), - balance, - epoch, - ) - .await - .0 - .get(&token); - if total_balance.is_zero() { - display_line!( - context.io(), - "No shielded {} balance found for given key", - token_alias - ); - } else { - display_line!( - context.io(), - "{}: {}", - token_alias, - context - .format_amount(&token, total_balance.into()) - .await - ); - } - } - } - // Here the user wants to know the balance of all tokens across users - (None, false) => { - // Maps asset types to balances divided by viewing key - let mut balances = HashMap::new(); - for fvk in viewing_keys { - // Query the multi-asset balance at the given spending key - let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; - let mut shielded = context.shielded_mut().await; - let balance = if no_conversions { - shielded - .compute_shielded_balance(&viewing_key) - .await - .unwrap() - .expect("context should contain viewing key") - } else { - shielded - .compute_exchanged_balance( - context.client(), - context.io(), - &viewing_key, - epoch, - ) - .await - .unwrap() - .expect("context should contain viewing key") - }; - let balance = shielded - .decode_combine_sum_to_epoch( - context.client(), - balance, - epoch, - ) - .await; - for (key, value) in balance.0.components() { - balances - .entry(key.clone()) - .or_insert(Vec::new()) - .push((fvk, *value)); - } - } - // Print non-zero balances whose asset types can be decoded - // TODO Implement a function for this + // Query the token alias in the wallet for pretty printing token balances + let token_alias = lookup_token_alias(context, &token, &MASP).await; - let mut balance_map = HashMap::new(); - for (token_addr, balances) in balances { - if balances.is_empty() { - display_line!( - context.io(), - "No shielded {} balance found for any wallet key", - &token_addr - ); - } - for (fvk, value) in balances { - balance_map.insert((fvk, token_addr.clone()), value); - } - } - for ((fvk, token), token_balance) in balance_map { - // Only assets with the current timestamp count - let alias = lookup_token_alias(context, &token, &MASP).await; - display_line!(context.io(), "Shielded Token {}:", alias); - let formatted = - context.format_amount(&token, token_balance.into()).await; - display_line!( - context.io(), - " {}, owned by {}", - formatted, - fvk - ); - } - } - // Here the user wants to know the balance for a specific token across - // users - (Some(base_token), false) => { - let tokens = query_tokens( - context, - Some(&base_token), - None, - args.show_ibc_tokens, - ) - .await; - for (token_alias, token) in tokens { - let mut found_any = false; - display_line!(context.io(), "Shielded Token {}:", token_alias); - for fvk in &viewing_keys { - // Query the multi-asset balance at the given spending key - let viewing_key = ExtendedFullViewingKey::from(*fvk).fvk.vk; - let mut shielded = context.shielded_mut().await; - let balance = if no_conversions { - shielded - .compute_shielded_balance(&viewing_key) - .await - .unwrap() - .expect("context should contain viewing key") - } else { - shielded - .compute_exchanged_balance( - context.client(), - context.io(), - &viewing_key, - epoch, - ) - .await - .unwrap() - .expect("context should contain viewing key") - }; + // Query the multi-asset balance at the given spending key + let mut shielded = context.shielded_mut().await; - let total_balance = shielded - .decode_combine_sum_to_epoch( - context.client(), - balance, - epoch, - ) - .await - .0 - .get(&token); - if !total_balance.is_zero() { - found_any = true; - } - let formatted = context - .format_amount(&token, total_balance.into()) - .await; - display_line!( - context.io(), - " {}, owned by {}", - formatted, - fvk - ); - } - if !found_any { - display_line!( - context.io(), - "No shielded {} balance found for any wallet key", - token_alias, - ); - } - } - } - // Here the user wants to know all possible token balances for a key - (None, true) => { - // Query the multi-asset balance at the given spending key - let viewing_key = - ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; - if no_conversions { - let balance = context - .shielded_mut() - .await - .compute_shielded_balance(&viewing_key) - .await - .unwrap() - .expect("context should contain viewing key"); - // Print balances by human-readable token names - print_decoded_balance_with_epoch(context, balance).await; - } else { - let balance = context - .shielded_mut() - .await - .compute_exchanged_balance( - context.client(), - context.io(), - &viewing_key, - epoch, - ) - .await - .unwrap() - .expect("context should contain viewing key"); - // Print balances by human-readable token names - print_decoded_balance(context, balance, epoch).await; - } - } - } -} + let no_balance = || { + display_line!(context.io(), "{token_alias}: 0"); + }; -pub async fn print_decoded_balance( - context: &impl Namada, - balance: I128Sum, - epoch: Epoch, -) { - if balance.is_zero() { - display_line!(context.io(), "No shielded balance found for given key"); - } else { - let decoded_balance = context - .shielded_mut() + let balance = if no_conversions { + let Some(bal) = shielded + .compute_shielded_balance(&viewing_key) .await - .decode_combine_sum_to_epoch(context.client(), balance, epoch) - .await; - for (token_addr, amount) in decoded_balance.0.components() { - display_line!( - context.io(), - "{} : {}", - lookup_token_alias(context, token_addr, &MASP).await, - context.format_amount(token_addr, (*amount).into()).await, - ); - } - for (asset_type, amount) in decoded_balance.1.components() { - display_line!(context.io(), "{} : {}", asset_type, amount,); - } - } -} - -pub async fn print_decoded_balance_with_epoch( - context: &impl Namada, - balance: I128Sum, -) { - let tokens = context - .wallet() - .await - .get_addresses_with_vp_type(AddressVpType::Token); - if balance.is_zero() { - display_line!(context.io(), "No shielded balance found for given key"); - } - let decoded_balance = context - .shielded_mut() - .await - .decode_combine_sum(context.client(), balance) - .await; - for ((epoch, token_addr), value) in decoded_balance.0.components() { - let asset_value = (*value).into(); - let alias = tokens - .get(token_addr) - .map(|a| a.to_string()) - .unwrap_or_else(|| token_addr.to_string()); - if let Some(epoch) = epoch { - display_line!( + .unwrap() + else { + no_balance(); + return; + }; + bal + } else { + let Some(bal) = shielded + .compute_exchanged_balance( + context.client(), context.io(), - "{} | {} : {}", - alias, + &viewing_key, epoch, - context.format_amount(token_addr, asset_value).await, - ); - } else { - display_line!( - context.io(), - "{} : {}", - alias, - context.format_amount(token_addr, asset_value).await, - ); - } - } - for (asset_type, value) in decoded_balance.1.components() { - display_line!(context.io(), "{} : {}", asset_type, value,); - } -} + ) + .await + .unwrap() + else { + no_balance(); + return; + }; + bal + }; -/// Query token amount of owner. -pub async fn get_token_balance( - client: &C, - token: &Address, - owner: &Address, -) -> token::Amount { - namada_sdk::rpc::get_token_balance(client, token, owner) + let total_balance = shielded + .decode_combine_sum_to_epoch(context.client(), balance, epoch) .await - .unwrap() + .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( @@ -2269,7 +1763,7 @@ pub async fn query_conversion( token::Denomination, MaspDigitPos, Epoch, - masp_primitives::transaction::components::I128Sum, + I128Sum, MerklePath, )> { namada_sdk::rpc::query_conversion(client, asset_type).await diff --git a/crates/apps/src/lib/client/tx.rs b/crates/apps/src/lib/client/tx.rs index 798e6f1822..e0b4588b3f 100644 --- a/crates/apps/src/lib/client/tx.rs +++ b/crates/apps/src/lib/client/tx.rs @@ -851,12 +851,13 @@ where e.to_string(), ) })?; - let author_balance = rpc::get_token_balance( + let author_balance = namada_sdk::rpc::get_token_balance( namada.client(), &namada.native_token(), &proposal.proposal.author, ) - .await; + .await + .unwrap(); let proposal = proposal .validate( &governance_parameters, @@ -879,12 +880,13 @@ where e.to_string(), ) })?; - let author_balane = rpc::get_token_balance( + let author_balane = namada_sdk::rpc::get_token_balance( namada.client(), &namada.native_token(), &proposal.proposal.author, ) - .await; + .await + .unwrap(); let proposal = proposal .validate( &governance_parameters, diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index acf869e8f9..bd3e9f6d58 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -1321,24 +1321,11 @@ pub struct QueryBalance { /// Common query args pub query: Query, /// Address of an owner - pub owner: Option, + pub owner: C::BalanceOwner, /// Address of a token - pub token: Option, + pub token: C::Address, /// Whether not to convert balances pub no_conversions: bool, - /// Show IBC tokens - pub show_ibc_tokens: bool, -} - -/// Query IBC token(s) -#[derive(Clone, Debug)] -pub struct QueryIbcToken { - /// Common query args - pub query: Query, - /// The token address which could be a non-namada address - pub token: Option, - /// Address of an owner - pub owner: Option, } /// Query historical transfer(s) diff --git a/crates/tests/src/integration/ledger_tests.rs b/crates/tests/src/integration/ledger_tests.rs index c983aaef53..4b044a1a8c 100644 --- a/crates/tests/src/integration/ledger_tests.rs +++ b/crates/tests/src/integration/ledger_tests.rs @@ -19,8 +19,8 @@ use test_log::test; use crate::e2e::ledger_tests::prepare_proposal_data; use crate::e2e::setup::constants::{ - ALBERT, ALBERT_KEY, BERTHA, BERTHA_KEY, CHRISTEL, CHRISTEL_KEY, DAEWON, - ESTER, GOVERNANCE_ADDRESS, NAM, + ALBERT, ALBERT_KEY, APFEL, BERTHA, BERTHA_KEY, BTC, CHRISTEL, CHRISTEL_KEY, + DAEWON, DOT, ESTER, ETH, GOVERNANCE_ADDRESS, KARTOFFEL, NAM, SCHNITZEL, }; use crate::integration::helpers::{ find_address, prepare_steward_commission_update_data, @@ -209,19 +209,80 @@ fn ledger_txs_and_queries() -> Result<()> { // expect a decimal vec![r"nam: \d+(\.\d+)?"], ), - // Unspecified token expect all tokens from wallet derived from genesis + // Test balance of tokens generated at genesis ( - vec!["balance", "--owner", ALBERT, "--node", &validator_one_rpc], - // expect all genesis tokens, sorted by alias vec![ - r"apfel: \d+(\.\d+)?", - r"btc: \d+(\.\d+)?", - r"dot: \d+(\.\d+)?", - r"eth: \d+(\.\d+)?", - r"kartoffel: \d+(\.\d+)?", - r"schnitzel: \d+(\.\d+)?", + "balance", + "--owner", + ALBERT, + "--token", + APFEL, + "--node", + &validator_one_rpc, + ], + vec![r"apfel: \d+(\.\d+)?"], + ), + ( + vec![ + "balance", + "--owner", + ALBERT, + "--token", + BTC, + "--node", + &validator_one_rpc, + ], + vec![r"btc: \d+(\.\d+)?"], + ), + ( + vec![ + "balance", + "--owner", + ALBERT, + "--token", + DOT, + "--node", + &validator_one_rpc, + ], + vec![r"dot: \d+(\.\d+)?"], + ), + ( + vec![ + "balance", + "--owner", + ALBERT, + "--token", + ETH, + "--node", + &validator_one_rpc, + ], + vec![r"eth: \d+(\.\d+)?"], + ), + ( + vec![ + "balance", + "--owner", + ALBERT, + "--token", + KARTOFFEL, + "--node", + &validator_one_rpc, + ], + vec![r"kartoffel: \d+(\.\d+)?"], + ), + ( + vec![ + "balance", + "--owner", + ALBERT, + "--token", + SCHNITZEL, + "--node", + &validator_one_rpc, ], + vec![r"schnitzel: \d+(\.\d+)?"], ), + // Account query ( vec![ "query-account", diff --git a/crates/tests/src/integration/masp.rs b/crates/tests/src/integration/masp.rs index 7af9565a70..92b9106ad9 100644 --- a/crates/tests/src/integration/masp.rs +++ b/crates/tests/src/integration/masp.rs @@ -109,7 +109,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found")); + assert!(captured.contains("nam: 0")); // Wait till epoch boundary node.next_epoch(); @@ -318,7 +318,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found")); + assert!(captured.contains("nam: 0")); // Wait till epoch boundary node.next_epoch(); @@ -439,7 +439,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded eth balance found")); + assert!(captured.contains("eth: 0")); node.next_epoch(); // sync the shielded context @@ -549,7 +549,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded btc balance found")); + assert!(captured.contains("btc: 0")); // Assert VK(A) retained the NAM rewards let captured = CapturedOutput::of(|| { @@ -750,7 +750,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found")); + assert!(captured.contains("nam: 0")); // sync the shielded context run( @@ -776,7 +776,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found")); + assert!(captured.contains("nam: 0")); // Assert NAM balance at MASP pool is nearly 0 let captured = CapturedOutput::of(|| { @@ -1132,7 +1132,7 @@ fn masp_txs_and_queries() -> Result<()> { "--node", validator_one_rpc, ], - Response::Ok("No shielded btc balance found"), + Response::Ok("btc: 0"), ), // 9. Assert ETH balance at VK(A) is 0 ( @@ -1145,7 +1145,7 @@ fn masp_txs_and_queries() -> Result<()> { "--node", validator_one_rpc, ], - Response::Ok("No shielded eth balance found"), + Response::Ok("eth: 0"), ), // 10. Assert balance at VK(B) is 20 BTC ( @@ -1153,10 +1153,12 @@ fn masp_txs_and_queries() -> Result<()> { "balance", "--owner", AB_VIEWING_KEY, + "--token", + BTC, "--node", validator_one_rpc, ], - Response::Ok("btc : 20"), + Response::Ok("btc: 20"), ), // 11. Send 20 BTC from SK(B) to Bertha ( @@ -2154,7 +2156,7 @@ fn dynamic_assets() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found for given key")); + assert!(captured.contains("nam: 0")); { // Start decoding and distributing shielded rewards for BTC in next @@ -2223,7 +2225,7 @@ fn dynamic_assets() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found for given key")); + assert!(captured.contains("nam: 0")); // Send 1 BTC from Albert to PA run( @@ -2288,7 +2290,7 @@ fn dynamic_assets() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found for given key")); + assert!(captured.contains("nam: 0")); // Wait till epoch boundary node.next_epoch();