penumbra_sdk_custody/
terminal.rs

1use anyhow::Result;
2use penumbra_sdk_governance::ValidatorVoteBody;
3use penumbra_sdk_proto::DomainType;
4use penumbra_sdk_stake::validator::Validator;
5use penumbra_sdk_transaction::TransactionPlan;
6use serde::de::DeserializeOwned;
7use tonic::async_trait;
8
9#[derive(Debug, Clone)]
10pub enum SigningRequest {
11    TransactionPlan(TransactionPlan),
12    ValidatorDefinition(Validator),
13    ValidatorVote(ValidatorVoteBody),
14}
15/// A trait abstracting over the kind of terminal interface we expect.
16///
17/// This is mainly used to accommodate the kind of interaction we have with the CLI
18/// interface, but it can also be plugged in with more general backends.
19#[async_trait]
20pub trait Terminal: Sync {
21    /// Have a user confirm that they want to sign this transaction or other data (e.g. validator
22    /// definition, validator vote)
23    ///
24    /// In an actual terminal, this should display the data to be signed in a human readable
25    /// form, and then get feedback from the user.
26    async fn confirm_request(&self, request: &SigningRequest) -> Result<bool>;
27
28    /// Push an explanatory message to the terminal.
29    ///
30    /// This message has no relation to the actual protocol, it just allows explaining
31    /// what subsequent data means, and what the user needs to do.
32    ///
33    /// Backends can replace this with a no-op.
34    fn explain(&self, msg: &str) -> Result<()>;
35
36    /// Broadcast a message to other users.
37    async fn broadcast(&self, data: &str) -> Result<()>;
38
39    /// Try to read a typed message from the terminal, retrying until
40    /// the message parses successfully or the user interrupts the program.
41    async fn next_response<D>(&self) -> Result<D>
42    where
43        D: DomainType,
44        anyhow::Error: From<<D as TryFrom<<D as DomainType>::Proto>>::Error>,
45        <D as DomainType>::Proto: DeserializeOwned,
46    {
47        loop {
48            // Read a line or bubble up an error if we couldn't.
49            let line = self.read_line_raw().await?;
50
51            let proto = match serde_json::from_str::<<D as DomainType>::Proto>(&line) {
52                Ok(proto) => proto,
53                Err(e) => {
54                    self.explain(&format!("Error parsing response: {:#}", e))?;
55                    self.explain("Please try again:")?;
56                    continue;
57                }
58            };
59
60            let message = match D::try_from(proto) {
61                Ok(message) => message,
62                Err(e) => {
63                    let e: anyhow::Error = e.into();
64                    self.explain(&format!("Error parsing response: {:#}", e))?;
65                    self.explain("Please try again:")?;
66                    continue;
67                }
68            };
69
70            return Ok(message);
71        }
72    }
73
74    /// Read a single line from the terminal.
75    async fn read_line_raw(&self) -> Result<String>;
76
77    /// Wait for the user to supply a password.
78    async fn get_password(&self) -> Result<String>;
79}