pcli/
network.rs

1use anyhow::Context;
2use decaf377_rdsa::{Signature, SpendAuth};
3use futures::{FutureExt, TryStreamExt};
4use penumbra_sdk_governance::ValidatorVoteBody;
5use penumbra_sdk_proto::{
6    custody::v1::{AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest},
7    util::tendermint_proxy::v1::tendermint_proxy_service_client::TendermintProxyServiceClient,
8    view::v1::broadcast_transaction_response::Status as BroadcastStatus,
9    DomainType,
10};
11use penumbra_sdk_stake::validator::Validator;
12use penumbra_sdk_transaction::{txhash::TransactionId, Transaction, TransactionPlan};
13use penumbra_sdk_view::{ViewClient, ViewServer};
14use std::{fs, future::Future};
15use tonic::transport::Channel;
16use tracing::instrument;
17
18use crate::App;
19
20impl App {
21    pub async fn build_and_submit_transaction(
22        &mut self,
23        plan: TransactionPlan,
24    ) -> anyhow::Result<TransactionId> {
25        let asset_cache = self.view().assets().await?;
26        println!(
27            "including transaction fee of {}...",
28            plan.transaction_parameters.fee.0.format(&asset_cache)
29        );
30
31        let transaction = self.build_transaction(plan).await?;
32        self.submit_transaction(transaction).await
33    }
34
35    pub fn build_transaction(
36        &mut self,
37        plan: TransactionPlan,
38    ) -> impl Future<Output = anyhow::Result<Transaction>> + '_ {
39        println!(
40            "building transaction [{} actions, {} proofs]...",
41            plan.actions.len(),
42            plan.num_proofs(),
43        );
44        let start = std::time::Instant::now();
45        let tx = penumbra_sdk_wallet::build_transaction(
46            &self.config.full_viewing_key,
47            self.view.as_mut().expect("view service initialized"),
48            &mut self.custody,
49            plan,
50        );
51        async move {
52            let tx = tx.await?;
53            let elapsed = start.elapsed();
54            println!(
55                "finished proving in {}.{:03} seconds [{} actions, {} proofs, {} bytes]",
56                elapsed.as_secs(),
57                elapsed.subsec_millis(),
58                tx.actions().count(),
59                tx.num_proofs(),
60                tx.encode_to_vec().len()
61            );
62            Ok(tx)
63        }
64    }
65
66    pub async fn sign_validator_definition(
67        &mut self,
68        validator_definition: Validator,
69    ) -> anyhow::Result<Signature<SpendAuth>> {
70        let request = AuthorizeValidatorDefinitionRequest {
71            validator_definition: Some(validator_definition.into()),
72            pre_authorizations: vec![],
73        };
74        self.custody
75            .authorize_validator_definition(request)
76            .await?
77            .into_inner()
78            .validator_definition_auth
79            .ok_or_else(|| anyhow::anyhow!("missing validator definition auth"))?
80            .try_into()
81    }
82
83    pub async fn sign_validator_vote(
84        &mut self,
85        validator_vote: ValidatorVoteBody,
86    ) -> anyhow::Result<Signature<SpendAuth>> {
87        let request = AuthorizeValidatorVoteRequest {
88            validator_vote: Some(validator_vote.into()),
89            pre_authorizations: vec![],
90        };
91        // Use the separate governance custody service, if one is configured, to sign the validator
92        // vote. This allows the governance custody service to have a different key than the main
93        // custody, which is useful for validators who want to have a separate key for voting.
94        self.governance_custody // VERY IMPORTANT: use governance custody here!
95            .authorize_validator_vote(request)
96            .await?
97            .into_inner()
98            .validator_vote_auth
99            .ok_or_else(|| anyhow::anyhow!("missing validator vote auth"))?
100            .try_into()
101    }
102
103    /// Submits a transaction to the network.
104    pub async fn submit_transaction(
105        &mut self,
106        transaction: Transaction,
107    ) -> anyhow::Result<TransactionId> {
108        if let Some(file) = &self.save_transaction_here_instead {
109            println!(
110                "saving transaction to disk, path: {}",
111                file.to_string_lossy()
112            );
113            fs::write(file, &serde_json::to_vec(&transaction)?)?;
114            return Ok(transaction.id());
115        }
116
117        println!("broadcasting transaction and awaiting confirmation...");
118        let mut rsp = self.view().broadcast_transaction(transaction, true).await?;
119
120        let id = async move {
121            while let Some(rsp) = rsp.try_next().await? {
122                match rsp.status {
123                    Some(status) => match status {
124                        BroadcastStatus::BroadcastSuccess(bs) => {
125                            println!(
126                                "transaction broadcast successfully: {}",
127                                TransactionId::try_from(
128                                    bs.id.expect("detected transaction missing id")
129                                )?
130                            );
131                        }
132                        BroadcastStatus::Confirmed(c) => {
133                            let id = c.id.expect("detected transaction missing id").try_into()?;
134                            if c.detection_height != 0 {
135                                println!(
136                                    "transaction confirmed and detected: {} @ height {}",
137                                    id, c.detection_height
138                                );
139                            } else {
140                                println!("transaction confirmed and detected: {}", id);
141                            }
142                            return Ok(id);
143                        }
144                    },
145                    None => {
146                        // No status is unexpected behavior
147                        return Err(anyhow::anyhow!(
148                            "empty BroadcastTransactionResponse message"
149                        ));
150                    }
151                }
152            }
153
154            Err(anyhow::anyhow!(
155                "should have received BroadcastTransaction status or error"
156            ))
157        }
158        .boxed()
159        .await
160        .context("error broadcasting transaction")?;
161
162        Ok(id)
163    }
164
165    /// Submits a transaction to the network, returning `Ok` as soon as the
166    /// transaction has been submitted, rather than waiting for confirmation.
167    #[instrument(skip(self, transaction))]
168    pub async fn submit_transaction_unconfirmed(
169        &mut self,
170        transaction: Transaction,
171    ) -> anyhow::Result<()> {
172        println!("broadcasting transaction without confirmation...");
173        self.view()
174            .broadcast_transaction(transaction, false)
175            .await?;
176
177        Ok(())
178    }
179
180    /// Convenience method for obtaining a `tonic::Channel` for the remote
181    /// `pd` endpoint, as configured for `pcli`.
182    pub async fn pd_channel(&self) -> anyhow::Result<Channel> {
183        ViewServer::get_pd_channel(self.config.grpc_url.clone())
184            .await
185            .context(format!("could not connect to {}", self.config.grpc_url))
186    }
187
188    pub async fn tendermint_proxy_client(
189        &self,
190    ) -> anyhow::Result<TendermintProxyServiceClient<Channel>> {
191        let channel = self.pd_channel().await?;
192        Ok(TendermintProxyServiceClient::new(channel))
193    }
194}