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 self.governance_custody .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 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 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 #[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 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}