diff --git a/crates/apps/src/lib/cli.rs b/crates/apps/src/lib/cli.rs index 049e49a2dc..25c81920e4 100644 --- a/crates/apps/src/lib/cli.rs +++ b/crates/apps/src/lib/cli.rs @@ -251,6 +251,7 @@ 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)) @@ -325,6 +326,7 @@ 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); @@ -388,6 +390,7 @@ 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) @@ -479,6 +482,7 @@ pub mod cmds { QueryMaspRewardTokens(QueryMaspRewardTokens), QueryBlock(QueryBlock), QueryBalance(QueryBalance), + QueryIbcToken(QueryIbcToken), QueryBonds(QueryBonds), QueryBondedStake(QueryBondedStake), QueryCommissionRate(QueryCommissionRate), @@ -1670,6 +1674,25 @@ pub mod cmds { } } + #[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); @@ -3182,6 +3205,7 @@ pub mod args { pub const TIMEOUT_SEC_OFFSET: ArgOpt = arg_opt("timeout-sec-offset"); pub const TM_ADDRESS: ArgOpt = arg_opt("tm-address"); pub const TOKEN_OPT: ArgOpt = TOKEN.opt(); + pub const TOKEN_STR_OPT: ArgOpt = TOKEN_STR.opt(); pub const TOKEN: Arg = arg("token"); pub const TOKEN_STR: Arg = arg("token"); pub const TRANSFER_SOURCE: Arg = arg("source"); @@ -5291,6 +5315,41 @@ pub mod args { } } + impl CliToSdk> for QueryIbcToken { + fn to_sdk(self, ctx: &mut Context) -> QueryIbcToken { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + 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."), + ) + } + } + impl CliToSdk> for QueryTransfers { fn to_sdk(self, ctx: &mut Context) -> QueryTransfers { let query = self.query.to_sdk(ctx); diff --git a/crates/apps/src/lib/cli/client.rs b/crates/apps/src/lib/cli/client.rs index 566cfab888..3bb2b1c206 100644 --- a/crates/apps/src/lib/cli/client.rs +++ b/crates/apps/src/lib/cli/client.rs @@ -530,6 +530,18 @@ 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/client/rpc.rs b/crates/apps/src/lib/client/rpc.rs index bea1f7345d..5b810caae8 100644 --- a/crates/apps/src/lib/client/rpc.rs +++ b/crates/apps/src/lib/client/rpc.rs @@ -16,7 +16,6 @@ use masp_primitives::transaction::components::I128Sum; use masp_primitives::zip32::ExtendedFullViewingKey; use namada::core::address::{Address, InternalAddress, MASP}; use namada::core::hash::Hash; -use namada::core::ibc::{is_ibc_denom, IbcTokenHash}; use namada::core::key::*; use namada::core::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; use namada::core::storage::{ @@ -39,9 +38,7 @@ use namada::governance::utils::{ }; use namada::io::Io; use namada::ledger::events::Event; -use namada::ledger::ibc::storage::{ - ibc_trace_key, ibc_trace_key_prefix, is_ibc_trace_key, -}; +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; @@ -731,7 +728,9 @@ async fn lookup_token_alias( match query_storage_value::<_, String>(context.client(), &ibc_trace_key) .await { - Ok(ibc_trace) => get_ibc_trace_alias(context, ibc_trace).await, + Ok(ibc_trace) => { + context.wallet().await.lookup_ibc_token_alias(ibc_trace) + } Err(_) => token.to_string(), } } else { @@ -763,60 +762,51 @@ async fn query_tokens( if tokens.is_empty() { base_token = None; } - let prefixes = match (base_token, owner) { - (Some(base_token), Some(owner)) => vec![ - ibc_trace_key_prefix(Some(base_token.to_string())), - ibc_trace_key_prefix(Some(owner.to_string())), - ], - (Some(base_token), None) => { - vec![ibc_trace_key_prefix(Some(base_token.to_string()))] - } - (None, Some(_)) => { - // Check all IBC denoms because the owner might not know IBC token - // transfers in the same chain - vec![ibc_trace_key_prefix(None)] - } - (None, None) => vec![ibc_trace_key_prefix(None)], - }; - - for prefix in prefixes { - let ibc_denoms = query_storage_prefix::(context, &prefix).await; - if let Some(ibc_denoms) = ibc_denoms { - for (key, ibc_trace) in ibc_denoms { - if let Some((_, hash)) = is_ibc_trace_key(&key) { - let ibc_denom_alias = - get_ibc_trace_alias(context, ibc_trace).await; - let hash: IbcTokenHash = hash.parse().expect( - "Parsing an IBC token hash from storage shouldn't fail", - ); - let ibc_token = - Address::Internal(InternalAddress::IbcToken(hash)); - tokens.insert(ibc_denom_alias, ibc_token); - } + 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 } -async fn get_ibc_trace_alias( +pub async fn query_ibc_tokens( context: &impl Namada, - ibc_trace: impl AsRef, -) -> String { + args: args::QueryIbcToken, +) { let wallet = context.wallet().await; - is_ibc_denom(&ibc_trace) - .map(|(trace_path, base_token)| { - let base_token_alias = match Address::decode(&base_token) { - Ok(base_token) => wallet.lookup_alias(&base_token), - Err(_) => base_token, - }; - if trace_path.is_empty() { - base_token_alias - } else { - format!("{}/{}", trace_path, base_token_alias) + 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); } - }) - .unwrap_or(ibc_trace.as_ref().to_string()) + } + Err(e) => { + edisplay_line!(context.io(), "IBC token query failed: {}", e); + } + } } /// Query votes for the given proposal diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index 11c31d83e8..0198c50064 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -214,6 +214,10 @@ where &msg.packet.port_id_on_b, &msg.packet.chan_id_on_b, )?; + if !ibc_denom.contains('/') { + // Skip to store it because the token has been redeemed + return Ok(()); + } let receiver = if PaymentAddress::from_str(data.receiver.as_ref()).is_ok() { MASP.to_string() diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index ce4ca4471d..d058710b37 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -1293,6 +1293,17 @@ pub struct QueryBalance { pub no_conversions: 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) #[derive(Clone, Debug)] pub struct QueryTransfers { diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index ac0abd9e25..ee56500e51 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -12,6 +12,7 @@ use masp_primitives::sapling::Node; use namada_account::Account; use namada_core::address::{Address, InternalAddress}; use namada_core::hash::Hash; +use namada_core::ibc::IbcTokenHash; use namada_core::key::common; use namada_core::storage::{ BlockHeight, BlockResults, Epoch, Key, PrefixValue, @@ -1360,6 +1361,48 @@ pub async fn format_denominated_amount( .to_string() } +/// Look up IBC tokens. The given base token can be non-Namada token. +pub async fn query_ibc_tokens( + context: &N, + base_token: Option, + owner: Option<&Address>, +) -> Result, Error> { + // Check the base token + let prefixes = match (base_token, owner) { + (Some(base_token), Some(owner)) => vec![ + ibc_trace_key_prefix(Some(base_token)), + ibc_trace_key_prefix(Some(owner.to_string())), + ], + (Some(base_token), None) => { + vec![ibc_trace_key_prefix(Some(base_token))] + } + _ => { + // Check all IBC denoms because the owner might not know IBC token + // transfers in the same chain + vec![ibc_trace_key_prefix(None)] + } + }; + + let mut tokens = BTreeMap::new(); + for prefix in prefixes { + let ibc_traces = + query_storage_prefix::<_, String>(context, &prefix).await?; + if let Some(ibc_traces) = ibc_traces { + for (key, ibc_trace) in ibc_traces { + if let Some((_, hash)) = is_ibc_trace_key(&key) { + let hash: IbcTokenHash = hash.parse().expect( + "Parsing an IBC token hash from storage shouldn't fail", + ); + let ibc_token = + Address::Internal(InternalAddress::IbcToken(hash)); + tokens.insert(ibc_trace, ibc_token); + } + } + } + } + Ok(tokens) +} + /// Look up the IBC denomination from a IbcToken. pub async fn query_ibc_denom( context: &N, diff --git a/crates/sdk/src/wallet/mod.rs b/crates/sdk/src/wallet/mod.rs index 4d90279c42..487fe841e0 100644 --- a/crates/sdk/src/wallet/mod.rs +++ b/crates/sdk/src/wallet/mod.rs @@ -13,6 +13,7 @@ use alias::Alias; use bip39::{Language, Mnemonic, MnemonicType, Seed}; use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::address::Address; +use namada_core::ibc::is_ibc_denom; use namada_core::key::*; use namada_core::masp::{ ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, @@ -379,6 +380,34 @@ impl Wallet { } } + /// Try to find an alias of the base token in the given IBC denomination + /// from the wallet. If not found, formats the IBC denomination into a + /// string. + pub fn lookup_ibc_token_alias(&self, ibc_denom: impl AsRef) -> String { + // Convert only an IBC denom or a Namada address since an NFT trace + // doesn't have the alias + is_ibc_denom(&ibc_denom) + .map(|(trace_path, base_token)| { + let base_token_alias = match Address::decode(&base_token) { + Ok(base_token) => self.lookup_alias(&base_token), + Err(_) => base_token, + }; + if trace_path.is_empty() { + base_token_alias + } else { + format!("{}/{}", trace_path, base_token_alias) + } + }) + .or_else(|| { + // It's not an IBC denom, but could be a raw Namada address + match Address::decode(&ibc_denom) { + Ok(addr) => Some(self.lookup_alias(&addr)), + Err(_) => None, + } + }) + .unwrap_or(ibc_denom.as_ref().to_string()) + } + /// Find the viewing key with the given alias in the wallet and return it pub fn find_viewing_key( &self,