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