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 ActionPlan::ActionLiquidityTournamentVote(_) => None,
164 }
165 }
166
167 println!("{}", serde_json::to_string_pretty(plan)?);
169
170 let fvk = match fvk {
172 None => {
173 return Ok(());
174 }
175 Some(x) => x,
176 };
177
178 let cache = Cache::with_known_assets();
179
180 let view = TransactionView {
181 anchor: penumbra_sdk_tct::Root(Hash::zero()),
182 binding_sig: dummy_sig(),
183 body_view: view::TransactionBodyView {
184 action_views: plan
185 .actions
186 .iter()
187 .filter_map(|x| convert_action(&cache, &fvk, x))
188 .collect(),
189 transaction_parameters: plan.transaction_parameters.clone(),
190 detection_data: None,
191 memo_view: None,
192 },
193 };
194
195 view.render_terminal();
196
197 Ok(())
198}
199
200#[derive(Clone, Default)]
204pub struct ActualTerminal {
205 pub fvk: Option<FullViewingKey>,
206}
207
208#[async_trait]
209impl Terminal for ActualTerminal {
210 async fn confirm_request(&self, signing_request: &SigningRequest) -> Result<bool> {
211 match signing_request {
212 SigningRequest::TransactionPlan(plan) => {
213 pretty_print_transaction_plan(self.fvk.clone(), plan)?;
214 println!("Do you approve this transaction?");
215 }
216 SigningRequest::ValidatorDefinition(def) => {
217 println!("{}", serde_json::to_string_pretty(def)?);
218 println!("Do you approve this validator definition?");
219 }
220 SigningRequest::ValidatorVote(vote) => {
221 println!("{}", serde_json::to_string_pretty(vote)?);
222 println!("Do you approve this validator vote?");
223 }
224 };
225
226 println!("Press enter to continue");
227 self.read_line_raw().await?;
228 Ok(true)
229 }
230
231 fn explain(&self, msg: &str) -> Result<()> {
232 println!(
233 "{}{}{}",
234 color::Fg(color::Blue),
235 msg,
236 color::Fg(color::Reset)
237 );
238 Ok(())
239 }
240
241 async fn broadcast(&self, data: &str) -> Result<()> {
242 println!(
243 "\n{}{}{}\n",
244 color::Fg(color::Yellow),
245 data,
246 color::Fg(color::Reset)
247 );
248 Ok(())
249 }
250
251 async fn read_line_raw(&self) -> Result<String> {
252 use termion::raw::IntoRawMode;
255 tracing::debug!("about to enter raw mode for long pasted input");
256
257 print!("{}", color::Fg(color::Red));
258 let mut stdout = std::io::stdout().into_raw_mode()?;
261
262 let mut bytes = Vec::with_capacity(8192);
263 for b in std::io::stdin().bytes() {
264 let b = b?;
265 if b == 3 || b == 4 {
267 return Err(anyhow::anyhow!("aborted"));
269 }
270 if b == b'\n' || b == b'\r' {
272 break;
273 }
274 bytes.push(b);
276 stdout.write_all(&[b]).expect("stdout write failed");
277 stdout.flush()?;
279 }
280 std::mem::drop(stdout);
282 println!("");
285 print!("{}", color::Fg(color::Reset));
286
287 tracing::debug!("exited raw mode and returned to cooked mode");
288
289 let line = String::from_utf8(bytes)?;
290 tracing::debug!(?line, "read response line");
291
292 Ok(line)
293 }
294
295 async fn get_password(&self) -> Result<String> {
296 read_password("Enter Password: ").await
297 }
298}
299
300impl ActualTerminal {
301 pub async fn get_confirmed_password() -> Result<String> {
302 loop {
303 let password = read_password("Enter Password: ").await?;
304 let confirmed = read_password("Confirm Password: ").await?;
305 if password != confirmed {
306 println!("Password mismatch, please try again.");
307 continue;
308 }
309 return Ok(password);
310 }
311 }
312}