pcli/command/
query.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
use anyhow::{anyhow, Context, Result};

pub(crate) mod auction;
mod chain;
mod community_pool;
mod dex;
mod governance;
mod ibc_query;
mod shielded_pool;
mod tx;
mod validator;

use auction::AuctionCmd;
use base64::prelude::*;
use chain::ChainCmd;
use colored_json::ToColoredJson;
use community_pool::CommunityPoolCmd;
use dex::DexCmd;
use governance::GovernanceCmd;
use ibc_query::IbcCmd;
use penumbra_proto::cnidarium::v1::non_verifiable_key_value_request::Key as NVKey;
use shielded_pool::ShieldedPool;
use tx::Tx;
pub(super) use validator::ValidatorCmd;

use crate::App;

#[derive(Clone, clap::ValueEnum, Debug)]
pub enum OutputFormat {
    Json,
    Base64,
}

impl Default for OutputFormat {
    fn default() -> Self {
        Self::Json
    }
}

#[derive(Debug, clap::Subcommand)]
pub enum QueryCmd {
    /// Queries an arbitrary key.
    Key {
        /// The key to query.
        ///
        /// When querying the JMT, keys are plain string values.
        ///
        /// When querying nonverifiable storage, keys should be base64-encoded strings.
        key: String,
        /// The storage backend to query.
        ///
        /// Valid arguments are "jmt" and "nonverifiable".
        ///
        /// Defaults to the JMT.
        #[clap(long, default_value = "jmt")]
        storage_backend: String,
    },
    /// Queries shielded pool data.
    #[clap(subcommand)]
    ShieldedPool(ShieldedPool),
    /// Queries a transaction by hash.
    Tx(Tx),
    /// Queries information about the chain.
    #[clap(subcommand)]
    Chain(ChainCmd),
    /// Queries information about validators.
    #[clap(subcommand)]
    Validator(ValidatorCmd),
    /// Queries information about governance proposals.
    #[clap(subcommand)]
    Governance(GovernanceCmd),
    /// Queries information about the Community Pool.
    #[clap(subcommand)]
    CommunityPool(CommunityPoolCmd),
    /// Queries information about the decentralized exchange.
    #[clap(subcommand)]
    Dex(DexCmd),
    /// Queries information about IBC.
    #[clap(subcommand)]
    Ibc(IbcCmd),
    /// Subscribes to a filtered stream of state changes.
    Watch {
        /// The regex to filter keys in verifiable storage.
        ///
        /// The empty string matches all keys; the pattern $^ matches no keys.
        #[clap(long, default_value = "")]
        key_regex: String,
        /// The regex to filter keys in nonverifiable storage.
        ///
        /// The empty string matches all keys; the pattern $^ matches no keys.
        #[clap(long, default_value = "")]
        nv_key_regex: String,
    },
    /// Queries information about a Dutch auction.
    #[clap(subcommand)]
    Auction(AuctionCmd),
}

impl QueryCmd {
    pub async fn exec(&self, app: &mut App) -> Result<()> {
        if let QueryCmd::Watch {
            key_regex,
            nv_key_regex,
        } = self
        {
            return watch(key_regex.clone(), nv_key_regex.clone(), app).await;
        }

        // Special-case: this is a Tendermint query
        if let QueryCmd::Tx(tx) = self {
            return tx.exec(app).await;
        }

        if let QueryCmd::Chain(chain) = self {
            return chain.exec(app).await;
        }

        if let QueryCmd::Validator(validator) = self {
            return validator.exec(app).await;
        }

        if let QueryCmd::Dex(dex) = self {
            return dex.exec(app).await;
        }

        if let QueryCmd::Governance(governance) = self {
            return governance.exec(app).await;
        }

        if let QueryCmd::CommunityPool(cp) = self {
            return cp.exec(app).await;
        }

        if let QueryCmd::Ibc(ibc) = self {
            return ibc.exec(app).await;
        }

        if let QueryCmd::Auction(auction) = self {
            return auction.exec(app).await;
        }

        // TODO: this is a hack; we should replace all raw state key uses with RPC methods.
        if let QueryCmd::ShieldedPool(ShieldedPool::CompactBlock { height }) = self {
            use penumbra_proto::core::component::compact_block::v1::{
                query_service_client::QueryServiceClient as CompactBlockQueryServiceClient,
                CompactBlockRequest,
            };
            let mut client = CompactBlockQueryServiceClient::new(app.pd_channel().await?);
            let compact_block = client
                .compact_block(CompactBlockRequest { height: *height })
                .await?
                .into_inner()
                .compact_block
                .ok_or_else(|| anyhow!("compact block missing from response"))?;
            let json = serde_json::to_string_pretty(&compact_block)?;

            println!("{}", json.to_colored_json_auto()?);
            return Ok(());
        }

        let (key, storage_backend) = match self {
            QueryCmd::Tx(_)
            | QueryCmd::Chain(_)
            | QueryCmd::Validator(_)
            | QueryCmd::Dex(_)
            | QueryCmd::Governance(_)
            | QueryCmd::CommunityPool(_)
            | QueryCmd::Watch { .. }
            | QueryCmd::Auction { .. }
            | QueryCmd::Ibc(_) => {
                unreachable!("query handled in guard");
            }
            QueryCmd::ShieldedPool(p) => (p.key().clone(), "jmt".to_string()),
            QueryCmd::Key {
                key,
                storage_backend,
            } => (key.clone(), storage_backend.clone()),
        };

        use penumbra_proto::cnidarium::v1::query_service_client::QueryServiceClient;
        let mut client = QueryServiceClient::new(app.pd_channel().await?);

        // Using an enum in the clap arguments was annoying; this is workable:
        match storage_backend.as_str() {
            "nonverifiable" => {
                let key_bytes = BASE64_STANDARD
                    .decode(&key)
                    .map_err(|e| anyhow::anyhow!(format!("invalid base64: {}", e)))?;

                let req = penumbra_proto::cnidarium::v1::NonVerifiableKeyValueRequest {
                    key: Some(NVKey { inner: key_bytes }),
                    ..Default::default()
                };

                tracing::debug!(?req);

                let value = client
                    .non_verifiable_key_value(req)
                    .await?
                    .into_inner()
                    .value
                    .context(format!("key not found! key={}", key))?;

                self.display_value(&value.value)?;
            }
            // Default to JMT
            "jmt" | _ => {
                let req = penumbra_proto::cnidarium::v1::KeyValueRequest {
                    key: key.clone(),
                    // Command-line queries don't have a reason to include proofs as of now.
                    proof: false,
                    ..Default::default()
                };

                tracing::debug!(?req);

                let value = client
                    .key_value(req)
                    .await?
                    .into_inner()
                    .value
                    .context(format!("key not found! key={}", key))?;

                self.display_value(&value.value)?;
            }
        };

        Ok(())
    }

    pub fn offline(&self) -> bool {
        match self {
            QueryCmd::Dex { .. } | QueryCmd::CommunityPool { .. } => false,
            QueryCmd::Tx { .. }
            | QueryCmd::Chain { .. }
            | QueryCmd::Validator { .. }
            | QueryCmd::ShieldedPool { .. }
            | QueryCmd::Governance { .. }
            | QueryCmd::Key { .. }
            | QueryCmd::Watch { .. }
            | QueryCmd::Auction { .. }
            | QueryCmd::Ibc(_) => true,
        }
    }

    fn display_value(&self, bytes: &[u8]) -> Result<()> {
        match self {
            QueryCmd::Key { .. } => {
                println!("{}", hex::encode(bytes));
            }
            QueryCmd::ShieldedPool(sp) => sp.display_value(bytes)?,
            QueryCmd::Tx { .. }
            | QueryCmd::Chain { .. }
            | QueryCmd::Validator { .. }
            | QueryCmd::Dex { .. }
            | QueryCmd::Governance { .. }
            | QueryCmd::CommunityPool { .. }
            | QueryCmd::Watch { .. }
            | QueryCmd::Auction { .. }
            | QueryCmd::Ibc(_) => {
                unreachable!("query is special cased")
            }
        }

        Ok(())
    }
}

// this code (not just this function, the whole module) is pretty shitty,
// but that's maybe okay for the moment. it exists to consume the rpc.
async fn watch(key_regex: String, nv_key_regex: String, app: &mut App) -> Result<()> {
    use penumbra_proto::cnidarium::v1::{
        query_service_client::QueryServiceClient, watch_response as wr,
    };
    let mut client = QueryServiceClient::new(app.pd_channel().await?);

    let req = penumbra_proto::cnidarium::v1::WatchRequest {
        key_regex,
        nv_key_regex,
    };

    tracing::debug!(?req);

    let mut stream = client.watch(req).await?.into_inner();

    while let Some(rsp) = stream.message().await? {
        match rsp.entry {
            Some(wr::Entry::Kv(kv)) => {
                if kv.deleted {
                    println!("{} KV {} -> DELETED", rsp.version, kv.key);
                } else {
                    println!(
                        "{} KV {} -> {}",
                        rsp.version,
                        kv.key,
                        simple_base64::encode(&kv.value)
                    );
                }
            }
            Some(wr::Entry::NvKv(nv_kv)) => {
                let key = simple_base64::encode(&nv_kv.key);

                if nv_kv.deleted {
                    println!("{} NVKV {} -> DELETED", rsp.version, key);
                } else {
                    println!(
                        "{} NVKV {} -> {}",
                        rsp.version,
                        key,
                        simple_base64::encode(&nv_kv.value)
                    );
                }
            }
            None => {
                return Err(anyhow!("server returned None event"));
            }
        }
    }

    Ok(())
}