1use comfy_table::presets;
2use comfy_table::Table;
3use penumbra_sdk_asset::asset::Id;
4use penumbra_sdk_asset::asset::Metadata;
5use penumbra_sdk_asset::Value;
6use penumbra_sdk_asset::ValueView;
7use penumbra_sdk_dex::swap::SwapView;
8use penumbra_sdk_dex::swap_claim::SwapClaimView;
9use penumbra_sdk_dex::PositionOpen;
10use penumbra_sdk_fee::Fee;
11use penumbra_sdk_keys::AddressView;
12use penumbra_sdk_num::Amount;
13use penumbra_sdk_shielded_pool::SpendView;
14use penumbra_sdk_transaction::view::action_view::OutputView;
15use penumbra_sdk_transaction::TransactionView;
16
17fn create_value_view(value: Value, metadata: Option<Metadata>) -> ValueView {
24 match metadata {
25 Some(metadata) => ValueView::KnownAssetId {
26 amount: value.amount,
27 metadata,
28 equivalent_values: Vec::new(),
29 extended_metadata: None,
30 },
31 None => ValueView::UnknownAssetId {
32 amount: value.amount,
33 asset_id: value.asset_id,
34 },
35 }
36}
37
38fn format_opaque_bytes(bytes: &[u8]) -> String {
40 if bytes.len() < 8 {
41 return String::new();
42 } else {
43 let max_bytes = 32;
57 let rem = if bytes.len() > max_bytes {
58 bytes[0..max_bytes].to_vec()
59 } else {
60 bytes.to_vec()
61 };
62
63 let hex_str = hex::encode_upper(rem);
65 let opaque_chars: String = hex_str
66 .chars()
67 .map(|c| {
68 match c {
69 '0' => "\u{2595}",
70 '1' => "\u{2581}",
71 '2' => "\u{2582}",
72 '3' => "\u{2583}",
73 '4' => "\u{2584}",
74 '5' => "\u{2585}",
75 '6' => "\u{2586}",
76 '7' => "\u{2587}",
77 '8' => "\u{2588}",
78 '9' => "\u{2589}",
79 'A' => "\u{259A}",
80 'B' => "\u{259B}",
81 'C' => "\u{259C}",
82 'D' => "\u{259D}",
83 'E' => "\u{259E}",
84 'F' => "\u{259F}",
85 _ => "",
86 }
87 .to_string()
88 })
89 .collect();
90
91 format!("{}", opaque_chars)
93 }
94}
95
96fn format_address_view(address_view: &AddressView) -> String {
99 match address_view {
100 AddressView::Decoded {
101 address: _,
102 index,
103 wallet_id: _,
104 } => {
105 if !index.is_ephemeral() {
106 format!("[account {:?}]", index.account)
107 } else {
108 format!("[account {:?} (one-time address)]", index.account)
109 }
110 }
111 AddressView::Opaque { address } => {
112 format!("{}", address)
115 }
116 }
117}
118
119fn format_value_view(value_view: &ValueView) -> String {
122 match value_view {
123 ValueView::KnownAssetId {
124 amount,
125 metadata: denom,
126 ..
127 } => {
128 let unit = denom.default_unit();
129 format!("{}{}", unit.format_value(*amount), unit)
130 }
131 ValueView::UnknownAssetId { amount, asset_id } => {
132 format!("{}{}", amount, asset_id)
133 }
134 }
135}
136
137fn format_amount_range(
138 start: Amount,
139 stop: Amount,
140 asset_id: &Id,
141 metadata: Option<&Metadata>,
142) -> String {
143 match metadata {
144 Some(denom) => {
145 let unit = denom.default_unit();
146 format!(
147 "({}..{}){}",
148 unit.format_value(start),
149 unit.format_value(stop),
150 unit
151 )
152 }
153 None => format!("({}..{}){}", start, stop, asset_id),
154 }
155}
156
157fn format_fee(fee: &Fee) -> String {
158 format!("{}", fee.amount())
160}
161
162fn format_asset_id(asset_id: &Id) -> String {
163 let input = &asset_id.to_string();
165 let truncated = &input[0..10]; let ellipsis = "...";
167 let end = &input[(input.len() - 3)..];
168 format!("{}{}{}", truncated, ellipsis, end)
169}
170
171fn value_view_amount(value_view: &ValueView) -> Amount {
175 match value_view {
176 ValueView::KnownAssetId { amount, .. } | ValueView::UnknownAssetId { amount, .. } => {
177 *amount
178 }
179 }
180}
181
182pub trait TransactionViewExt {
183 fn render_terminal(&self);
185}
186
187impl TransactionViewExt for TransactionView {
188 fn render_terminal(&self) {
189 let fee = &self.body_view.transaction_parameters.fee;
190 println!("Fee: {}", format_fee(&fee));
192
193 println!(
194 "Expiration Height: {}",
195 &self.body_view.transaction_parameters.expiry_height
196 );
197
198 if let Some(memo_view) = &self.body_view.memo_view {
199 match memo_view {
200 penumbra_sdk_transaction::MemoView::Visible {
201 plaintext,
202 ciphertext: _,
203 } => {
204 println!("Memo Sender: {}", &plaintext.return_address.address());
205 println!("Memo Text: \n{}\n", &plaintext.text);
206 }
207 penumbra_sdk_transaction::MemoView::Opaque { ciphertext } => {
208 println!("Encrypted Memo: \n{}\n", format_opaque_bytes(&ciphertext.0));
209 }
210 }
211 }
212
213 let mut actions_table = Table::new();
214 actions_table.load_preset(presets::NOTHING);
215 actions_table.set_header(vec!["Tx Action", "Description"]);
216
217 for action_view in &self.body_view.action_views {
219 let action: String;
220
221 let row = match action_view {
222 penumbra_sdk_transaction::ActionView::Spend(spend) => {
223 match spend {
224 SpendView::Visible { spend: _, note } => {
225 action = format!(
226 "{} -> {}",
227 format_address_view(¬e.address),
228 format_value_view(¬e.value)
229 );
230 ["Spend", &action]
231 }
232 SpendView::Opaque { spend } => {
233 let bytes = spend.body.nullifier.to_bytes(); action = format_opaque_bytes(&bytes);
235 ["Spend", &action]
236 }
237 }
238 }
239 penumbra_sdk_transaction::ActionView::Output(output) => {
240 match output {
241 OutputView::Visible {
242 output: _,
243 note,
244 payload_key: _,
245 } => {
246 action = format!(
247 "{} -> {}",
248 format_value_view(¬e.value),
249 format_address_view(¬e.address),
250 );
251 ["Output", &action]
252 }
253 OutputView::Opaque { output } => {
254 let bytes = output.body.note_payload.encrypted_note.0; action = format_opaque_bytes(&bytes);
256 ["Output", &action]
257 }
258 }
259 }
260 penumbra_sdk_transaction::ActionView::Swap(swap) => {
261 match swap {
263 SwapView::Visible { swap_plaintext, .. } => {
264 let (from_asset, from_value, to_asset) = match (
265 swap_plaintext.delta_1_i.value(),
266 swap_plaintext.delta_2_i.value(),
267 ) {
268 (0, v) if v > 0 => (
269 swap_plaintext.trading_pair.asset_2(),
270 swap_plaintext.delta_2_i,
271 swap_plaintext.trading_pair.asset_1(),
272 ),
273 (v, 0) if v > 0 => (
274 swap_plaintext.trading_pair.asset_1(),
275 swap_plaintext.delta_1_i,
276 swap_plaintext.trading_pair.asset_2(),
277 ),
278 _ => (
280 swap_plaintext.trading_pair.asset_1(),
281 swap_plaintext.delta_1_i,
282 swap_plaintext.trading_pair.asset_1(),
283 ),
284 };
285
286 action = format!(
287 "{} {} for {} and paid claim fee {}",
288 from_value,
289 format_asset_id(&from_asset),
290 format_asset_id(&to_asset),
291 format_fee(&swap_plaintext.claim_fee),
292 );
293
294 ["Swap", &action]
295 }
296 SwapView::Opaque { swap, .. } => {
297 action = format!(
298 "Opaque swap for trading pair: {} <=> {}",
299 format_asset_id(&swap.body.trading_pair.asset_1()),
300 format_asset_id(&swap.body.trading_pair.asset_2()),
301 );
302 ["Swap", &action]
303 }
304 }
305 }
306 penumbra_sdk_transaction::ActionView::SwapClaim(swap_claim) => {
307 match swap_claim {
308 SwapClaimView::Visible {
309 swap_claim,
310 output_1,
311 output_2,
312 swap_tx: _,
313 } => {
314 dbg!(swap_claim);
316 let claimed_value = match (
317 value_view_amount(&output_1.value).value(),
318 value_view_amount(&output_2.value).value(),
319 ) {
320 (0, v) if v > 0 => format_value_view(&output_2.value),
321 (v, 0) if v > 0 => format_value_view(&output_1.value),
322 _ => format!(
324 "{} and {}",
325 format_value_view(&output_1.value),
326 format_value_view(&output_2.value),
327 ),
328 };
329
330 action = format!(
331 "Claimed {} with fee {:?}",
332 claimed_value,
333 format_fee(&swap_claim.body.fee),
334 );
335 ["Swap Claim", &action]
336 }
337 SwapClaimView::Opaque { swap_claim } => {
338 let bytes = swap_claim.body.nullifier.to_bytes(); action = format_opaque_bytes(&bytes);
340 ["Swap Claim", &action]
341 }
342 }
343 }
344 penumbra_sdk_transaction::ActionView::Ics20Withdrawal(withdrawal) => {
345 let unit = withdrawal.denom.best_unit_for(withdrawal.amount);
346 action = format!(
347 "{}{} via {} to {}",
348 unit.format_value(withdrawal.amount),
349 unit,
350 withdrawal.source_channel,
351 withdrawal.destination_chain_address,
352 );
353 ["Ics20 Withdrawal", &action]
354 }
355 penumbra_sdk_transaction::ActionView::PositionOpen(position_open) => {
356 let position = PositionOpen::from(position_open.clone()).position;
357 action = format!(
365 "Reserves: ({} {}, {} {}) Fee: {} ID: {}",
366 position.reserves.r1,
367 format_asset_id(&position.phi.pair.asset_1()),
368 position.reserves.r2,
369 format_asset_id(&position.phi.pair.asset_2()),
370 position.phi.component.fee,
371 position.id(),
372 );
373 ["Open Liquidity Position", &action]
374 }
375 penumbra_sdk_transaction::ActionView::PositionClose(_) => {
376 ["Close Liquitity Position", ""]
377 }
378 penumbra_sdk_transaction::ActionView::PositionWithdraw(_) => {
379 ["Withdraw Liquitity Position", ""]
380 }
381 penumbra_sdk_transaction::ActionView::ProposalDepositClaim(
382 proposal_deposit_claim,
383 ) => {
384 action = format!(
385 "Claim Deposit for Governance Proposal #{}",
386 proposal_deposit_claim.proposal
387 );
388 [&action, ""]
389 }
390 penumbra_sdk_transaction::ActionView::ProposalSubmit(proposal_submit) => {
391 action = format!(
392 "Submit Governance Proposal #{}",
393 proposal_submit.proposal.id
394 );
395 [&action, ""]
396 }
397 penumbra_sdk_transaction::ActionView::ProposalWithdraw(proposal_withdraw) => {
398 action = format!(
399 "Withdraw Governance Proposal #{}",
400 proposal_withdraw.proposal
401 );
402 [&action, ""]
403 }
404 penumbra_sdk_transaction::ActionView::IbcRelay(_) => ["IBC Relay", ""],
405 penumbra_sdk_transaction::ActionView::DelegatorVote(_) => ["Delegator Vote", ""],
406 penumbra_sdk_transaction::ActionView::ValidatorDefinition(_) => {
407 ["Upload Validator Definition", ""]
408 }
409 penumbra_sdk_transaction::ActionView::ValidatorVote(_) => ["Validator Vote", ""],
410 penumbra_sdk_transaction::ActionView::CommunityPoolDeposit(_) => {
411 ["Community Pool Deposit", ""]
412 }
413 penumbra_sdk_transaction::ActionView::CommunityPoolSpend(_) => {
414 ["Community Pool Spend", ""]
415 }
416 penumbra_sdk_transaction::ActionView::CommunityPoolOutput(_) => {
417 ["Community Pool Output", ""]
418 }
419 penumbra_sdk_transaction::ActionView::Delegate(_) => ["Delegation", ""],
420 penumbra_sdk_transaction::ActionView::Undelegate(_) => ["Undelegation", ""],
421 penumbra_sdk_transaction::ActionView::UndelegateClaim(_) => {
422 ["Undelegation Claim", ""]
423 }
424 penumbra_sdk_transaction::ActionView::ActionDutchAuctionSchedule(x) => {
425 let description = &x.action.description;
426
427 let input: String = format_value_view(&create_value_view(
428 description.input,
429 x.input_metadata.clone(),
430 ));
431 let output: String = format_amount_range(
432 description.min_output,
433 description.max_output,
434 &description.output_id,
435 x.output_metadata.as_ref(),
436 );
437 let start = description.start_height;
438 let stop = description.end_height;
439 let steps = description.step_count;
440 let auction_id = x.auction_id;
441 action = format!(
442 "{} -> {}, blocks {}..{}, in {} steps ({})",
443 input, output, start, stop, steps, auction_id
444 );
445 ["Dutch Auction Schedule", &action]
446 }
447 penumbra_sdk_transaction::ActionView::ActionDutchAuctionEnd(x) => {
448 action = format!("{}", x.auction_id);
449 ["Dutch Auction End", &action]
450 }
451 penumbra_sdk_transaction::ActionView::ActionDutchAuctionWithdraw(x) => {
452 let inside = x
453 .reserves
454 .iter()
455 .map(|value| format_value_view(value))
456 .collect::<Vec<_>>()
457 .as_slice()
458 .join(", ");
459 action = format!("{} -> [{}]", x.action.auction_id, inside);
460 ["Dutch Auction Withdraw", &action]
461 }
462 penumbra_sdk_transaction::ActionView::ActionLiquidityTournamentVote(_) => todo!(),
463 };
464
465 actions_table.add_row(row);
466 }
467
468 println!("{actions_table}");
470 }
471}