1use std::io::{IsTerminal, Read, Write};
2
3use anyhow::Result;
4use decaf377::{Element, Fq};
5use decaf377_rdsa::{Domain, Signature, VerificationKey};
6use penumbra_sdk_asset::{asset::Cache, balance::Commitment};
7use penumbra_sdk_custody::threshold::{SigningRequest, Terminal};
8use penumbra_sdk_keys::{
9 symmetric::{OvkWrappedKey, WrappedMemoKey},
10 FullViewingKey, PayloadKey,
11};
12use penumbra_sdk_proof_params::GROTH16_PROOF_LENGTH_BYTES;
13use penumbra_sdk_sct::Nullifier;
14use penumbra_sdk_shielded_pool::{EncryptedBackref, Note, NoteView};
15use penumbra_sdk_tct::structure::Hash;
16use penumbra_sdk_transaction::{view, ActionPlan, ActionView, TransactionPlan, TransactionView};
17use termion::{color, input::TermRead};
18use tonic::async_trait;
19
20use crate::transaction_view_ext::TransactionViewExt as _;
21
22async fn read_password(prompt: &str) -> Result<String> {
23 fn get_possibly_empty_string(prompt: &str) -> Result<String> {
24 if std::io::stdin().is_terminal() {
28 Ok(rpassword::prompt_password(prompt)?)
29 } else {
30 Ok(std::io::stdin().lock().read_line()?.unwrap_or_default())
31 }
32 }
33
34 let mut string: String = Default::default();
35 while string.is_empty() {
36 string = get_possibly_empty_string(prompt)?;
38 }
39 Ok(string)
40}
41
42fn pretty_print_transaction_plan(
43 fvk: Option<FullViewingKey>,
44 plan: &TransactionPlan,
45) -> anyhow::Result<()> {
46 use penumbra_sdk_shielded_pool::{output, spend};
47
48 fn dummy_sig<D: Domain>() -> Signature<D> {
49 Signature::from([0u8; 64])
50 }
51
52 fn dummy_pk<D: Domain>() -> VerificationKey<D> {
53 VerificationKey::try_from(Element::default().vartime_compress().0)
54 .expect("creating a dummy verification key should work")
55 }
56
57 fn dummy_commitment() -> Commitment {
58 Commitment(Element::default())
59 }
60
61 fn dummy_proof_spend() -> spend::SpendProof {
62 spend::SpendProof::try_from(
63 penumbra_sdk_proto::penumbra::core::component::shielded_pool::v1::ZkSpendProof {
64 inner: vec![0u8; GROTH16_PROOF_LENGTH_BYTES],
65 },
66 )
67 .expect("creating a dummy proof should work")
68 }
69
70 fn dummy_proof_output() -> output::OutputProof {
71 output::OutputProof::try_from(
72 penumbra_sdk_proto::penumbra::core::component::shielded_pool::v1::ZkOutputProof {
73 inner: vec![0u8; GROTH16_PROOF_LENGTH_BYTES],
74 },
75 )
76 .expect("creating a dummy proof should work")
77 }
78
79 fn dummy_spend() -> spend::Spend {
80 spend::Spend {
81 body: spend::Body {
82 balance_commitment: dummy_commitment(),
83 nullifier: Nullifier(Fq::default()),
84 rk: dummy_pk(),
85 encrypted_backref: EncryptedBackref::try_from([0u8; 0])
86 .expect("can create dummy encrypted backref"),
87 },
88 auth_sig: dummy_sig(),
89 proof: dummy_proof_spend(),
90 }
91 }
92
93 fn dummy_output() -> output::Output {
94 output::Output {
95 body: output::Body {
96 note_payload: penumbra_sdk_shielded_pool::NotePayload {
97 note_commitment: penumbra_sdk_shielded_pool::note::StateCommitment(
98 Fq::default(),
99 ),
100 ephemeral_key: [0u8; 32]
101 .as_slice()
102 .try_into()
103 .expect("can create dummy ephemeral key"),
104 encrypted_note: penumbra_sdk_shielded_pool::NoteCiphertext([0u8; 176]),
105 },
106 balance_commitment: dummy_commitment(),
107 ovk_wrapped_key: OvkWrappedKey([0u8; 48]),
108 wrapped_memo_key: WrappedMemoKey([0u8; 48]),
109 },
110 proof: dummy_proof_output(),
111 }
112 }
113
114 fn convert_note(cache: &Cache, fvk: &FullViewingKey, note: &Note) -> NoteView {
115 NoteView {
116 value: note.value().view_with_cache(cache),
117 rseed: note.rseed(),
118 address: fvk.view_address(note.address()),
119 }
120 }
121
122 fn convert_action(
123 cache: &Cache,
124 fvk: &FullViewingKey,
125 action: &ActionPlan,
126 ) -> Option<ActionView> {
127 use view::action_view::SpendView;
128
129 match action {
130 ActionPlan::Output(x) => Some(ActionView::Output(
131 penumbra_sdk_shielded_pool::OutputView::Visible {
132 output: dummy_output(),
133 note: convert_note(cache, fvk, &x.output_note()),
134 payload_key: PayloadKey::from([0u8; 32]),
135 },
136 )),
137 ActionPlan::Spend(x) => Some(ActionView::Spend(SpendView::Visible {
138 spend: dummy_spend(),
139 note: convert_note(cache, fvk, &x.note),
140 })),
141 ActionPlan::ValidatorDefinition(_) => None,
142 ActionPlan::Swap(_) => None,
143 ActionPlan::SwapClaim(_) => None,
144 ActionPlan::ProposalSubmit(_) => None,
145 ActionPlan::ProposalWithdraw(_) => None,
146 ActionPlan::DelegatorVote(_) => None,
147 ActionPlan::ValidatorVote(_) => None,
148 ActionPlan::ProposalDepositClaim(_) => None,
149 ActionPlan::PositionOpen(_) => None,
150 ActionPlan::PositionClose(_) => None,
151 ActionPlan::PositionWithdraw(_) => None,
152 ActionPlan::Delegate(_) => None,
153 ActionPlan::Undelegate(_) => None,
154 ActionPlan::UndelegateClaim(_) => None,
155 ActionPlan::Ics20Withdrawal(_) => None,
156 ActionPlan::CommunityPoolSpend(_) => None,
157 ActionPlan::CommunityPoolOutput(_) => None,
158 ActionPlan::CommunityPoolDeposit(_) => None,
159 ActionPlan::ActionDutchAuctionSchedule(_) => None,
160 ActionPlan::ActionDutchAuctionEnd(_) => None,
161 ActionPlan::ActionDutchAuctionWithdraw(_) => None,
162 ActionPlan::IbcAction(_) => todo!(),
163 }
164 }
165
166 println!("{}", serde_json::to_string_pretty(plan)?);
168
169 let fvk = match fvk {
171 None => {
172 return Ok(());
173 }
174 Some(x) => x,
175 };
176
177 let cache = Cache::with_known_assets();
178
179 let view = TransactionView {
180 anchor: penumbra_sdk_tct::Root(Hash::zero()),
181 binding_sig: dummy_sig(),
182 body_view: view::TransactionBodyView {
183 action_views: plan
184 .actions
185 .iter()
186 .filter_map(|x| convert_action(&cache, &fvk, x))
187 .collect(),
188 transaction_parameters: plan.transaction_parameters.clone(),
189 detection_data: None,
190 memo_view: None,
191 },
192 };
193
194 view.render_terminal();
195
196 Ok(())
197}
198
199#[derive(Clone, Default)]
203pub struct ActualTerminal {
204 pub fvk: Option<FullViewingKey>,
205}
206
207#[async_trait]
208impl Terminal for ActualTerminal {
209 async fn confirm_request(&self, signing_request: &SigningRequest) -> Result<bool> {
210 match signing_request {
211 SigningRequest::TransactionPlan(plan) => {
212 pretty_print_transaction_plan(self.fvk.clone(), plan)?;
213 println!("Do you approve this transaction?");
214 }
215 SigningRequest::ValidatorDefinition(def) => {
216 println!("{}", serde_json::to_string_pretty(def)?);
217 println!("Do you approve this validator definition?");
218 }
219 SigningRequest::ValidatorVote(vote) => {
220 println!("{}", serde_json::to_string_pretty(vote)?);
221 println!("Do you approve this validator vote?");
222 }
223 };
224
225 println!("Press enter to continue");
226 self.read_line_raw().await?;
227 Ok(true)
228 }
229
230 fn explain(&self, msg: &str) -> Result<()> {
231 println!(
232 "{}{}{}",
233 color::Fg(color::Blue),
234 msg,
235 color::Fg(color::Reset)
236 );
237 Ok(())
238 }
239
240 async fn broadcast(&self, data: &str) -> Result<()> {
241 println!(
242 "\n{}{}{}\n",
243 color::Fg(color::Yellow),
244 data,
245 color::Fg(color::Reset)
246 );
247 Ok(())
248 }
249
250 async fn read_line_raw(&self) -> Result<String> {
251 use termion::raw::IntoRawMode;
254 tracing::debug!("about to enter raw mode for long pasted input");
255
256 print!("{}", color::Fg(color::Red));
257 let mut stdout = std::io::stdout().into_raw_mode()?;
260
261 let mut bytes = Vec::with_capacity(8192);
262 for b in std::io::stdin().bytes() {
263 let b = b?;
264 if b == 3 || b == 4 {
266 return Err(anyhow::anyhow!("aborted"));
268 }
269 if b == b'\n' || b == b'\r' {
271 break;
272 }
273 bytes.push(b);
275 stdout.write_all(&[b]).expect("stdout write failed");
276 stdout.flush()?;
278 }
279 std::mem::drop(stdout);
281 println!("");
284 print!("{}", color::Fg(color::Reset));
285
286 tracing::debug!("exited raw mode and returned to cooked mode");
287
288 let line = String::from_utf8(bytes)?;
289 tracing::debug!(?line, "read response line");
290
291 Ok(line)
292 }
293
294 async fn get_password(&self) -> Result<String> {
295 read_password("Enter Password: ").await
296 }
297}
298
299impl ActualTerminal {
300 pub async fn get_confirmed_password() -> Result<String> {
301 loop {
302 let password = read_password("Enter Password: ").await?;
303 let confirmed = read_password("Confirm Password: ").await?;
304 if password != confirmed {
305 println!("Password mismatch, please try again.");
306 continue;
307 }
308 return Ok(password);
309 }
310 }
311}