pcli/command/
query.rs

1use anyhow::{anyhow, Context, Result};
2
3pub(crate) mod auction;
4mod chain;
5mod community_pool;
6mod dex;
7mod governance;
8mod ibc_query;
9mod shielded_pool;
10mod tx;
11mod validator;
12
13use auction::AuctionCmd;
14use base64::prelude::*;
15use chain::ChainCmd;
16use cnidarium::proto::v1::non_verifiable_key_value_request::Key as NVKey;
17use colored_json::ToColoredJson;
18use community_pool::CommunityPoolCmd;
19use dex::DexCmd;
20use governance::GovernanceCmd;
21use ibc_query::IbcCmd;
22use shielded_pool::ShieldedPool;
23use tx::Tx;
24pub(super) use validator::ValidatorCmd;
25
26use crate::App;
27
28#[derive(Clone, clap::ValueEnum, Debug)]
29pub enum OutputFormat {
30    Json,
31    Base64,
32}
33
34impl Default for OutputFormat {
35    fn default() -> Self {
36        Self::Json
37    }
38}
39
40#[derive(Debug, clap::Subcommand)]
41pub enum QueryCmd {
42    /// Queries an arbitrary key.
43    Key {
44        /// The key to query.
45        ///
46        /// When querying the JMT, keys are plain string values.
47        ///
48        /// When querying nonverifiable storage, keys should be base64-encoded strings.
49        key: String,
50        /// The storage backend to query.
51        ///
52        /// Valid arguments are "jmt" and "nonverifiable".
53        ///
54        /// Defaults to the JMT.
55        #[clap(long, default_value = "jmt")]
56        storage_backend: String,
57    },
58    /// Queries shielded pool data.
59    #[clap(subcommand)]
60    ShieldedPool(ShieldedPool),
61    /// Queries a transaction by hash.
62    Tx(Tx),
63    /// Queries information about the chain.
64    #[clap(subcommand)]
65    Chain(ChainCmd),
66    /// Queries information about validators.
67    #[clap(subcommand)]
68    Validator(ValidatorCmd),
69    /// Queries information about governance proposals.
70    #[clap(subcommand)]
71    Governance(GovernanceCmd),
72    /// Queries information about the Community Pool.
73    #[clap(subcommand)]
74    CommunityPool(CommunityPoolCmd),
75    /// Queries information about the decentralized exchange.
76    #[clap(subcommand)]
77    Dex(DexCmd),
78    /// Queries information about IBC.
79    #[clap(subcommand)]
80    Ibc(IbcCmd),
81    /// Subscribes to a filtered stream of state changes.
82    Watch {
83        /// The regex to filter keys in verifiable storage.
84        ///
85        /// The empty string matches all keys; the pattern $^ matches no keys.
86        #[clap(long, default_value = "")]
87        key_regex: String,
88        /// The regex to filter keys in nonverifiable storage.
89        ///
90        /// The empty string matches all keys; the pattern $^ matches no keys.
91        #[clap(long, default_value = "")]
92        nv_key_regex: String,
93    },
94    /// Queries information about a Dutch auction.
95    #[clap(subcommand)]
96    Auction(AuctionCmd),
97}
98
99impl QueryCmd {
100    pub async fn exec(&self, app: &mut App) -> Result<()> {
101        if let QueryCmd::Watch {
102            key_regex,
103            nv_key_regex,
104        } = self
105        {
106            return watch(key_regex.clone(), nv_key_regex.clone(), app).await;
107        }
108
109        // Special-case: this is a Tendermint query
110        if let QueryCmd::Tx(tx) = self {
111            return tx.exec(app).await;
112        }
113
114        if let QueryCmd::Chain(chain) = self {
115            return chain.exec(app).await;
116        }
117
118        if let QueryCmd::Validator(validator) = self {
119            return validator.exec(app).await;
120        }
121
122        if let QueryCmd::Dex(dex) = self {
123            return dex.exec(app).await;
124        }
125
126        if let QueryCmd::Governance(governance) = self {
127            return governance.exec(app).await;
128        }
129
130        if let QueryCmd::CommunityPool(cp) = self {
131            return cp.exec(app).await;
132        }
133
134        if let QueryCmd::Ibc(ibc) = self {
135            return ibc.exec(app).await;
136        }
137
138        if let QueryCmd::Auction(auction) = self {
139            return auction.exec(app).await;
140        }
141
142        // TODO: this is a hack; we should replace all raw state key uses with RPC methods.
143        if let QueryCmd::ShieldedPool(ShieldedPool::CompactBlock { height }) = self {
144            use penumbra_sdk_proto::core::component::compact_block::v1::{
145                query_service_client::QueryServiceClient as CompactBlockQueryServiceClient,
146                CompactBlockRequest,
147            };
148            let mut client = CompactBlockQueryServiceClient::new(app.pd_channel().await?);
149            let compact_block = client
150                .compact_block(CompactBlockRequest { height: *height })
151                .await?
152                .into_inner()
153                .compact_block
154                .ok_or_else(|| anyhow!("compact block missing from response"))?;
155            let json = serde_json::to_string_pretty(&compact_block)?;
156
157            println!("{}", json.to_colored_json_auto()?);
158            return Ok(());
159        }
160
161        let (key, storage_backend) = match self {
162            QueryCmd::Tx(_)
163            | QueryCmd::Chain(_)
164            | QueryCmd::Validator(_)
165            | QueryCmd::Dex(_)
166            | QueryCmd::Governance(_)
167            | QueryCmd::CommunityPool(_)
168            | QueryCmd::Watch { .. }
169            | QueryCmd::Auction { .. }
170            | QueryCmd::Ibc(_) => {
171                unreachable!("query handled in guard");
172            }
173            QueryCmd::ShieldedPool(p) => (p.key().clone(), "jmt".to_string()),
174            QueryCmd::Key {
175                key,
176                storage_backend,
177            } => (key.clone(), storage_backend.clone()),
178        };
179
180        use cnidarium::proto::v1::query_service_client::QueryServiceClient;
181        let mut client = QueryServiceClient::new(app.pd_channel().await?);
182
183        // Using an enum in the clap arguments was annoying; this is workable:
184        match storage_backend.as_str() {
185            "nonverifiable" => {
186                let key_bytes = BASE64_STANDARD
187                    .decode(&key)
188                    .map_err(|e| anyhow::anyhow!(format!("invalid base64: {}", e)))?;
189
190                let req = cnidarium::proto::v1::NonVerifiableKeyValueRequest {
191                    key: Some(NVKey { inner: key_bytes }),
192                    ..Default::default()
193                };
194
195                tracing::debug!(?req);
196
197                let value = client
198                    .non_verifiable_key_value(req)
199                    .await?
200                    .into_inner()
201                    .value
202                    .context(format!("key not found! key={}", key))?;
203
204                self.display_value(&value.value)?;
205            }
206            // Default to JMT
207            "jmt" | _ => {
208                let req = cnidarium::proto::v1::KeyValueRequest {
209                    key: key.clone(),
210                    // Command-line queries don't have a reason to include proofs as of now.
211                    proof: false,
212                    ..Default::default()
213                };
214
215                tracing::debug!(?req);
216
217                let value = client
218                    .key_value(req)
219                    .await?
220                    .into_inner()
221                    .value
222                    .context(format!("key not found! key={}", key))?;
223
224                self.display_value(&value.value)?;
225            }
226        };
227
228        Ok(())
229    }
230
231    pub fn offline(&self) -> bool {
232        match self {
233            QueryCmd::Dex { .. } | QueryCmd::CommunityPool { .. } => false,
234            QueryCmd::Tx { .. }
235            | QueryCmd::Chain { .. }
236            | QueryCmd::Validator { .. }
237            | QueryCmd::ShieldedPool { .. }
238            | QueryCmd::Governance { .. }
239            | QueryCmd::Key { .. }
240            | QueryCmd::Watch { .. }
241            | QueryCmd::Auction { .. }
242            | QueryCmd::Ibc(_) => true,
243        }
244    }
245
246    fn display_value(&self, bytes: &[u8]) -> Result<()> {
247        match self {
248            QueryCmd::Key { .. } => {
249                println!("{}", hex::encode(bytes));
250            }
251            QueryCmd::ShieldedPool(sp) => sp.display_value(bytes)?,
252            QueryCmd::Tx { .. }
253            | QueryCmd::Chain { .. }
254            | QueryCmd::Validator { .. }
255            | QueryCmd::Dex { .. }
256            | QueryCmd::Governance { .. }
257            | QueryCmd::CommunityPool { .. }
258            | QueryCmd::Watch { .. }
259            | QueryCmd::Auction { .. }
260            | QueryCmd::Ibc(_) => {
261                unreachable!("query is special cased")
262            }
263        }
264
265        Ok(())
266    }
267}
268
269// this code (not just this function, the whole module) is pretty shitty,
270// but that's maybe okay for the moment. it exists to consume the rpc.
271async fn watch(key_regex: String, nv_key_regex: String, app: &mut App) -> Result<()> {
272    use cnidarium::proto::v1::{query_service_client::QueryServiceClient, watch_response as wr};
273    let mut client = QueryServiceClient::new(app.pd_channel().await?);
274
275    let req = cnidarium::proto::v1::WatchRequest {
276        key_regex,
277        nv_key_regex,
278    };
279
280    tracing::debug!(?req);
281
282    let mut stream = client.watch(req).await?.into_inner();
283
284    while let Some(rsp) = stream.message().await? {
285        match rsp.entry {
286            Some(wr::Entry::Kv(kv)) => {
287                if kv.deleted {
288                    println!("{} KV {} -> DELETED", rsp.version, kv.key);
289                } else {
290                    println!(
291                        "{} KV {} -> {}",
292                        rsp.version,
293                        kv.key,
294                        simple_base64::encode(&kv.value)
295                    );
296                }
297            }
298            Some(wr::Entry::NvKv(nv_kv)) => {
299                let key = simple_base64::encode(&nv_kv.key);
300
301                if nv_kv.deleted {
302                    println!("{} NVKV {} -> DELETED", rsp.version, key);
303                } else {
304                    println!(
305                        "{} NVKV {} -> {}",
306                        rsp.version,
307                        key,
308                        simple_base64::encode(&nv_kv.value)
309                    );
310                }
311            }
312            None => {
313                return Err(anyhow!("server returned None event"));
314            }
315        }
316    }
317
318    Ok(())
319}