1use anyhow::Context;
2use decaf377_rdsa::{Binding, Signature};
3use penumbra_sdk_asset::{Balance, Value};
4use penumbra_sdk_dex::{swap::SwapView, swap_claim::SwapClaimView};
5use penumbra_sdk_keys::AddressView;
6use penumbra_sdk_proto::{core::transaction::v1 as pbt, DomainType};
7use penumbra_sdk_shielded_pool::{OutputView, SpendView};
8use serde::{Deserialize, Serialize};
9
10pub mod action_view;
11mod transaction_perspective;
12
13pub use action_view::ActionView;
14use penumbra_sdk_tct as tct;
15pub use transaction_perspective::TransactionPerspective;
16
17use crate::{
18 memo::MemoCiphertext,
19 transaction::{TransactionEffect, TransactionSummary},
20 Action, DetectionData, Transaction, TransactionBody, TransactionParameters,
21};
22
23#[derive(Clone, Debug, Serialize, Deserialize)]
24#[serde(try_from = "pbt::TransactionView", into = "pbt::TransactionView")]
25pub struct TransactionView {
26 pub body_view: TransactionBodyView,
27 pub binding_sig: Signature<Binding>,
28 pub anchor: tct::Root,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
32#[serde(
33 try_from = "pbt::TransactionBodyView",
34 into = "pbt::TransactionBodyView"
35)]
36pub struct TransactionBodyView {
37 pub action_views: Vec<ActionView>,
38 pub transaction_parameters: TransactionParameters,
39 pub detection_data: Option<DetectionData>,
40 pub memo_view: Option<MemoView>,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44#[serde(try_from = "pbt::MemoView", into = "pbt::MemoView")]
45#[allow(clippy::large_enum_variant)]
46pub enum MemoView {
47 Visible {
48 plaintext: MemoPlaintextView,
49 ciphertext: MemoCiphertext,
50 },
51 Opaque {
52 ciphertext: MemoCiphertext,
53 },
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize)]
57#[serde(try_from = "pbt::MemoPlaintextView", into = "pbt::MemoPlaintextView")]
58pub struct MemoPlaintextView {
59 pub return_address: AddressView,
60 pub text: String,
61}
62
63impl TransactionView {
64 pub fn transaction(&self) -> Transaction {
65 let mut actions = Vec::new();
66
67 for action_view in &self.body_view.action_views {
68 actions.push(Action::from(action_view.clone()));
69 }
70
71 let memo_ciphertext = match &self.body_view.memo_view {
72 Some(memo_view) => match memo_view {
73 MemoView::Visible {
74 plaintext: _,
75 ciphertext,
76 } => Some(ciphertext),
77 MemoView::Opaque { ciphertext } => Some(ciphertext),
78 },
79 None => None,
80 };
81
82 let transaction_parameters = self.body_view.transaction_parameters.clone();
83 let detection_data = self.body_view.detection_data.clone();
84
85 Transaction {
86 transaction_body: TransactionBody {
87 actions,
88 transaction_parameters,
89 detection_data,
90 memo: memo_ciphertext.cloned(),
91 },
92 binding_sig: self.binding_sig,
93 anchor: self.anchor,
94 }
95 }
96
97 pub fn action_views(&self) -> impl Iterator<Item = &ActionView> {
98 self.body_view.action_views.iter()
99 }
100
101 fn accumulate_effects(summary: TransactionSummary) -> TransactionSummary {
104 use std::collections::BTreeMap;
105 let mut keyed_effects: BTreeMap<AddressView, Balance> = BTreeMap::new();
106 for effect in summary.effects {
107 *keyed_effects.entry(effect.address).or_default() += effect.balance;
108 }
109 TransactionSummary {
110 effects: keyed_effects
111 .into_iter()
112 .map(|(address, balance)| TransactionEffect { address, balance })
113 .collect(),
114 }
115 }
116
117 pub fn summary(&self) -> TransactionSummary {
119 let mut effects = Vec::new();
120
121 for action_view in &self.body_view.action_views {
122 match action_view {
123 ActionView::Spend(spend_view) => match spend_view {
124 SpendView::Visible { spend: _, note } => {
125 let balance = Balance::from(note.value.value());
127
128 let address = AddressView::Opaque {
129 address: note.address(),
130 };
131
132 effects.push(TransactionEffect { address, balance });
133 }
134 SpendView::Opaque { spend: _ } => continue,
135 },
136 ActionView::Output(output_view) => match output_view {
137 OutputView::Visible {
138 output: _,
139 note,
140 payload_key: _,
141 } => {
142 let balance = -Balance::from(note.value.value());
144
145 let address = AddressView::Opaque {
146 address: note.address(),
147 };
148
149 effects.push(TransactionEffect { address, balance });
150 }
151 OutputView::Opaque { output: _ } => continue,
152 },
153 ActionView::Swap(swap_view) => match swap_view {
154 SwapView::Visible {
155 swap: _,
156 swap_plaintext,
157 output_1,
158 output_2: _,
159 claim_tx: _,
160 asset_1_metadata: _,
161 asset_2_metadata: _,
162 batch_swap_output_data: _,
163 } => {
164 let address = AddressView::Opaque {
165 address: output_1.clone().expect("sender address").address(),
166 };
167
168 let value_fee = Value {
169 amount: swap_plaintext.claim_fee.amount(),
170 asset_id: swap_plaintext.claim_fee.asset_id(),
171 };
172 let value_1 = Value {
173 amount: swap_plaintext.delta_1_i,
174 asset_id: swap_plaintext.trading_pair.asset_1(),
175 };
176 let value_2 = Value {
177 amount: swap_plaintext.delta_2_i,
178 asset_id: swap_plaintext.trading_pair.asset_2(),
179 };
180
181 let mut balance = Balance::default();
183 balance -= value_1;
184 balance -= value_2;
185 balance -= value_fee;
186
187 effects.push(TransactionEffect { address, balance });
188 }
189 SwapView::Opaque {
190 swap: _,
191 batch_swap_output_data: _,
192 output_1: _,
193 output_2: _,
194 asset_1_metadata: _,
195 asset_2_metadata: _,
196 } => continue,
197 },
198 ActionView::SwapClaim(swap_claim_view) => match swap_claim_view {
199 SwapClaimView::Visible {
200 swap_claim,
201 output_1,
202 output_2: _,
203 swap_tx: _,
204 } => {
205 let address = AddressView::Opaque {
206 address: output_1.address(),
207 };
208
209 let value_fee = Value {
210 amount: swap_claim.body.fee.amount(),
211 asset_id: swap_claim.body.fee.asset_id(),
212 };
213
214 let mut balance = Balance::default();
216 balance += value_fee;
217
218 effects.push(TransactionEffect { address, balance });
219 }
220 SwapClaimView::Opaque { swap_claim: _ } => continue,
221 },
222 _ => {} }
224 }
225
226 let summary = TransactionSummary { effects };
227
228 Self::accumulate_effects(summary)
229 }
230}
231
232impl DomainType for TransactionView {
233 type Proto = pbt::TransactionView;
234}
235
236impl TryFrom<pbt::TransactionView> for TransactionView {
237 type Error = anyhow::Error;
238
239 fn try_from(v: pbt::TransactionView) -> Result<Self, Self::Error> {
240 let binding_sig = v
241 .binding_sig
242 .ok_or_else(|| anyhow::anyhow!("transaction view missing binding signature"))?
243 .try_into()
244 .context("transaction binding signature malformed")?;
245
246 let anchor = v
247 .anchor
248 .ok_or_else(|| anyhow::anyhow!("transaction view missing anchor"))?
249 .try_into()
250 .context("transaction anchor malformed")?;
251
252 let body_view = v
253 .body_view
254 .ok_or_else(|| anyhow::anyhow!("transaction view missing body"))?
255 .try_into()
256 .context("transaction body malformed")?;
257
258 Ok(Self {
259 body_view,
260 binding_sig,
261 anchor,
262 })
263 }
264}
265
266impl TryFrom<pbt::TransactionBodyView> for TransactionBodyView {
267 type Error = anyhow::Error;
268
269 fn try_from(body_view: pbt::TransactionBodyView) -> Result<Self, Self::Error> {
270 let mut action_views = Vec::<ActionView>::new();
271 for av in body_view.action_views.clone() {
272 action_views.push(av.try_into()?);
273 }
274
275 let memo_view: Option<MemoView> = match body_view.memo_view {
276 Some(mv) => match mv.memo_view {
277 Some(x) => match x {
278 pbt::memo_view::MemoView::Visible(v) => Some(MemoView::Visible {
279 plaintext: v
280 .plaintext
281 .ok_or_else(|| {
282 anyhow::anyhow!("transaction view memo missing memo plaintext")
283 })?
284 .try_into()?,
285 ciphertext: v
286 .ciphertext
287 .ok_or_else(|| {
288 anyhow::anyhow!("transaction view memo missing memo ciphertext")
289 })?
290 .try_into()?,
291 }),
292 pbt::memo_view::MemoView::Opaque(v) => Some(MemoView::Opaque {
293 ciphertext: v
294 .ciphertext
295 .ok_or_else(|| {
296 anyhow::anyhow!("transaction view memo missing memo ciphertext")
297 })?
298 .try_into()?,
299 }),
300 },
301 None => None,
302 },
303 None => None,
304 };
305
306 let transaction_parameters = body_view
307 .transaction_parameters
308 .ok_or_else(|| anyhow::anyhow!("transaction view missing transaction parameters view"))?
309 .try_into()?;
310
311 let fmd_clues = body_view
313 .detection_data
314 .map(|dd| {
315 dd.fmd_clues
316 .into_iter()
317 .map(|fmd| fmd.try_into())
318 .collect::<Result<Vec<_>, _>>()
319 })
320 .transpose()?;
321
322 let detection_data = fmd_clues.map(|fmd_clues| DetectionData { fmd_clues });
323
324 Ok(TransactionBodyView {
325 action_views,
326 transaction_parameters,
327 detection_data,
328 memo_view,
329 })
330 }
331}
332
333impl From<TransactionView> for pbt::TransactionView {
334 fn from(v: TransactionView) -> Self {
335 Self {
336 body_view: Some(v.body_view.into()),
337 anchor: Some(v.anchor.into()),
338 binding_sig: Some(v.binding_sig.into()),
339 }
340 }
341}
342
343impl From<TransactionBodyView> for pbt::TransactionBodyView {
344 fn from(v: TransactionBodyView) -> Self {
345 Self {
346 action_views: v.action_views.into_iter().map(Into::into).collect(),
347 transaction_parameters: Some(v.transaction_parameters.into()),
348 detection_data: v.detection_data.map(Into::into),
349 memo_view: v.memo_view.map(|m| m.into()),
350 }
351 }
352}
353
354impl From<MemoView> for pbt::MemoView {
355 fn from(v: MemoView) -> Self {
356 Self {
357 memo_view: match v {
358 MemoView::Visible {
359 plaintext,
360 ciphertext,
361 } => Some(pbt::memo_view::MemoView::Visible(pbt::memo_view::Visible {
362 plaintext: Some(plaintext.into()),
363 ciphertext: Some(ciphertext.into()),
364 })),
365 MemoView::Opaque { ciphertext } => {
366 Some(pbt::memo_view::MemoView::Opaque(pbt::memo_view::Opaque {
367 ciphertext: Some(ciphertext.into()),
368 }))
369 }
370 },
371 }
372 }
373}
374
375impl TryFrom<pbt::MemoView> for MemoView {
376 type Error = anyhow::Error;
377
378 fn try_from(v: pbt::MemoView) -> Result<Self, Self::Error> {
379 match v
380 .memo_view
381 .ok_or_else(|| anyhow::anyhow!("missing memo field"))?
382 {
383 pbt::memo_view::MemoView::Visible(x) => Ok(MemoView::Visible {
384 plaintext: x
385 .plaintext
386 .ok_or_else(|| anyhow::anyhow!("missing plaintext field"))?
387 .try_into()?,
388 ciphertext: x
389 .ciphertext
390 .ok_or_else(|| anyhow::anyhow!("missing ciphertext field"))?
391 .try_into()?,
392 }),
393 pbt::memo_view::MemoView::Opaque(x) => Ok(MemoView::Opaque {
394 ciphertext: x
395 .ciphertext
396 .ok_or_else(|| anyhow::anyhow!("missing ciphertext field"))?
397 .try_into()?,
398 }),
399 }
400 }
401}
402
403impl From<MemoPlaintextView> for pbt::MemoPlaintextView {
404 fn from(v: MemoPlaintextView) -> Self {
405 Self {
406 return_address: Some(v.return_address.into()),
407 text: v.text,
408 }
409 }
410}
411
412impl TryFrom<pbt::MemoPlaintextView> for MemoPlaintextView {
413 type Error = anyhow::Error;
414
415 fn try_from(v: pbt::MemoPlaintextView) -> Result<Self, Self::Error> {
416 let sender: AddressView = v
417 .return_address
418 .ok_or_else(|| anyhow::anyhow!("memo plan missing memo plaintext"))?
419 .try_into()
420 .context("return address malformed")?;
421
422 let text: String = v.text;
423
424 Ok(Self {
425 return_address: sender,
426 text,
427 })
428 }
429}
430
431#[cfg(test)]
432mod test {
433 use super::*;
434
435 use decaf377::Fr;
436 use decaf377::{Element, Fq};
437 use decaf377_rdsa::{Domain, VerificationKey};
438 use penumbra_sdk_asset::{
439 asset::{self, Cache, Id},
440 balance::Commitment,
441 STAKING_TOKEN_ASSET_ID,
442 };
443 use penumbra_sdk_dex::swap::proof::SwapProof;
444 use penumbra_sdk_dex::swap::{SwapCiphertext, SwapPayload};
445 use penumbra_sdk_dex::Swap;
446 use penumbra_sdk_dex::{
447 swap::{SwapPlaintext, SwapPlan},
448 TradingPair,
449 };
450 use penumbra_sdk_fee::Fee;
451 use penumbra_sdk_keys::keys::Bip44Path;
452 use penumbra_sdk_keys::keys::{SeedPhrase, SpendKey};
453 use penumbra_sdk_keys::{
454 symmetric::{OvkWrappedKey, WrappedMemoKey},
455 test_keys, Address, FullViewingKey, PayloadKey,
456 };
457 use penumbra_sdk_num::Amount;
458 use penumbra_sdk_proof_params::GROTH16_PROOF_LENGTH_BYTES;
459 use penumbra_sdk_sct::Nullifier;
460 use penumbra_sdk_shielded_pool::Rseed;
461 use penumbra_sdk_shielded_pool::{output, spend, Note, NoteView, OutputPlan, SpendPlan};
462 use penumbra_sdk_tct::structure::Hash;
463 use penumbra_sdk_tct::StateCommitment;
464 use rand_core::OsRng;
465 use std::ops::Deref;
466
467 use crate::{
468 plan::{CluePlan, DetectionDataPlan},
469 view, ActionPlan, TransactionPlan,
470 };
471
472 #[cfg(test)]
473 fn dummy_sig<D: Domain>() -> Signature<D> {
474 Signature::from([0u8; 64])
475 }
476
477 #[cfg(test)]
478 fn dummy_pk<D: Domain>() -> VerificationKey<D> {
479 VerificationKey::try_from(Element::default().vartime_compress().0)
480 .expect("creating a dummy verification key should work")
481 }
482
483 #[cfg(test)]
484 fn dummy_commitment() -> Commitment {
485 Commitment(Element::default())
486 }
487
488 #[cfg(test)]
489 fn dummy_proof_spend() -> spend::SpendProof {
490 spend::SpendProof::try_from(
491 penumbra_sdk_proto::penumbra::core::component::shielded_pool::v1::ZkSpendProof {
492 inner: vec![0u8; GROTH16_PROOF_LENGTH_BYTES],
493 },
494 )
495 .expect("creating a dummy proof should work")
496 }
497
498 #[cfg(test)]
499 fn dummy_proof_output() -> output::OutputProof {
500 output::OutputProof::try_from(
501 penumbra_sdk_proto::penumbra::core::component::shielded_pool::v1::ZkOutputProof {
502 inner: vec![0u8; GROTH16_PROOF_LENGTH_BYTES],
503 },
504 )
505 .expect("creating a dummy proof should work")
506 }
507
508 #[cfg(test)]
509 fn dummy_proof_swap() -> SwapProof {
510 SwapProof::try_from(
511 penumbra_sdk_proto::penumbra::core::component::dex::v1::ZkSwapProof {
512 inner: vec![0u8; GROTH16_PROOF_LENGTH_BYTES],
513 },
514 )
515 .expect("creating a dummy proof should work")
516 }
517
518 #[cfg(test)]
519 fn dummy_spend() -> spend::Spend {
520 use penumbra_sdk_shielded_pool::EncryptedBackref;
521
522 spend::Spend {
523 body: spend::Body {
524 balance_commitment: dummy_commitment(),
525 nullifier: Nullifier(Fq::default()),
526 rk: dummy_pk(),
527 encrypted_backref: EncryptedBackref::dummy(),
528 },
529 auth_sig: dummy_sig(),
530 proof: dummy_proof_spend(),
531 }
532 }
533
534 #[cfg(test)]
535 fn dummy_output() -> output::Output {
536 output::Output {
537 body: output::Body {
538 note_payload: penumbra_sdk_shielded_pool::NotePayload {
539 note_commitment: penumbra_sdk_shielded_pool::note::StateCommitment(
540 Fq::default(),
541 ),
542 ephemeral_key: [0u8; 32]
543 .as_slice()
544 .try_into()
545 .expect("can create dummy ephemeral key"),
546 encrypted_note: penumbra_sdk_shielded_pool::NoteCiphertext([0u8; 176]),
547 },
548 balance_commitment: dummy_commitment(),
549 ovk_wrapped_key: OvkWrappedKey([0u8; 48]),
550 wrapped_memo_key: WrappedMemoKey([0u8; 48]),
551 },
552 proof: dummy_proof_output(),
553 }
554 }
555
556 #[cfg(test)]
557 fn dummy_swap_plaintext() -> SwapPlaintext {
558 let seed_phrase = SeedPhrase::generate(OsRng);
559 let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
560 let fvk_recipient = sk_recipient.full_viewing_key();
561 let ivk_recipient = fvk_recipient.incoming();
562 let (claim_address, _dtk_d) = ivk_recipient.payment_address(0u32.into());
563
564 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
565 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
566 let trading_pair = TradingPair::new(gm.id(), gn.id());
567
568 let delta_1 = Amount::from(1u64);
569 let delta_2 = Amount::from(0u64);
570 let fee = Fee::default();
571
572 let swap_plaintext = SwapPlaintext::new(
573 &mut OsRng,
574 trading_pair,
575 delta_1,
576 delta_2,
577 fee,
578 claim_address,
579 );
580
581 swap_plaintext
582 }
583
584 #[cfg(test)]
585 fn dummy_swap() -> Swap {
586 use penumbra_sdk_dex::swap::Body;
587
588 let seed_phrase = SeedPhrase::generate(OsRng);
589 let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
590 let fvk_recipient = sk_recipient.full_viewing_key();
591 let ivk_recipient = fvk_recipient.incoming();
592 let (claim_address, _dtk_d) = ivk_recipient.payment_address(0u32.into());
593
594 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
595 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
596 let trading_pair = TradingPair::new(gm.id(), gn.id());
597
598 let delta_1 = Amount::from(1u64);
599 let delta_2 = Amount::from(0u64);
600 let fee = Fee::default();
601
602 let swap_plaintext = SwapPlaintext::new(
603 &mut OsRng,
604 trading_pair,
605 delta_1,
606 delta_2,
607 fee,
608 claim_address,
609 );
610
611 let fee_blinding = Fr::from(0u64);
612 let fee_commitment = swap_plaintext.claim_fee.commit(fee_blinding);
613
614 let swap_payload = SwapPayload {
615 encrypted_swap: SwapCiphertext([0u8; 272]),
616 commitment: StateCommitment::try_from([0; 32]).expect("state commitment"),
617 };
618
619 Swap {
620 body: Body {
621 trading_pair: trading_pair,
622 delta_1_i: delta_1,
623 delta_2_i: delta_2,
624 fee_commitment: fee_commitment,
625 payload: swap_payload,
626 },
627 proof: dummy_proof_swap(),
628 }
629 }
630
631 #[cfg(test)]
632 fn dummy_note_view(
633 address: Address,
634 value: Value,
635 cache: &Cache,
636 fvk: &FullViewingKey,
637 ) -> NoteView {
638 let note = Note::from_parts(address, value, Rseed::generate(&mut OsRng))
639 .expect("generate dummy note");
640
641 NoteView {
642 value: note.value().view_with_cache(cache),
643 rseed: note.rseed(),
644 address: fvk.view_address(note.address()),
645 }
646 }
647
648 #[cfg(test)]
649 fn convert_note(cache: &Cache, fvk: &FullViewingKey, note: &Note) -> NoteView {
650 NoteView {
651 value: note.value().view_with_cache(cache),
652 rseed: note.rseed(),
653 address: fvk.view_address(note.address()),
654 }
655 }
656
657 #[cfg(test)]
658 fn convert_action(
659 cache: &Cache,
660 fvk: &FullViewingKey,
661 action: &ActionPlan,
662 ) -> Option<ActionView> {
663 use view::action_view::SpendView;
664
665 match action {
666 ActionPlan::Output(x) => Some(ActionView::Output(
667 penumbra_sdk_shielded_pool::OutputView::Visible {
668 output: dummy_output(),
669 note: convert_note(cache, fvk, &x.output_note()),
670 payload_key: PayloadKey::from([0u8; 32]),
671 },
672 )),
673 ActionPlan::Spend(x) => Some(ActionView::Spend(SpendView::Visible {
674 spend: dummy_spend(),
675 note: convert_note(cache, fvk, &x.note),
676 })),
677 ActionPlan::ValidatorDefinition(_) => None,
678 ActionPlan::Swap(x) => Some(ActionView::Swap(SwapView::Visible {
679 swap: dummy_swap(),
680 swap_plaintext: dummy_swap_plaintext(),
681 output_1: Some(dummy_note_view(
682 x.swap_plaintext.claim_address.clone(),
683 x.swap_plaintext.claim_fee.0,
684 cache,
685 fvk,
686 )),
687 output_2: None,
688 claim_tx: None,
689 asset_1_metadata: None,
690 asset_2_metadata: None,
691 batch_swap_output_data: None,
692 })),
693 ActionPlan::SwapClaim(_) => None,
694 ActionPlan::ProposalSubmit(_) => None,
695 ActionPlan::ProposalWithdraw(_) => None,
696 ActionPlan::DelegatorVote(_) => None,
697 ActionPlan::ValidatorVote(_) => None,
698 ActionPlan::ProposalDepositClaim(_) => None,
699 ActionPlan::PositionOpen(_) => None,
700 ActionPlan::PositionClose(_) => None,
701 ActionPlan::PositionWithdraw(_) => None,
702 ActionPlan::Delegate(_) => None,
703 ActionPlan::Undelegate(_) => None,
704 ActionPlan::UndelegateClaim(_) => None,
705 ActionPlan::Ics20Withdrawal(_) => None,
706 ActionPlan::CommunityPoolSpend(_) => None,
707 ActionPlan::CommunityPoolOutput(_) => None,
708 ActionPlan::CommunityPoolDeposit(_) => None,
709 ActionPlan::ActionDutchAuctionSchedule(_) => None,
710 ActionPlan::ActionDutchAuctionEnd(_) => None,
711 ActionPlan::ActionDutchAuctionWithdraw(_) => None,
712 ActionPlan::IbcAction(_) => todo!(),
713 }
714 }
715
716 #[test]
717 fn test_internal_transfer_transaction_summary() {
718 let value = Value {
720 amount: 100u64.into(),
721 asset_id: *STAKING_TOKEN_ASSET_ID,
722 };
723 let note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
724
725 let value2 = Value {
726 amount: 50u64.into(),
727 asset_id: Id(Fq::rand(&mut OsRng)),
728 };
729 let note2 = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value2);
730
731 let value3 = Value {
732 amount: 75u64.into(),
733 asset_id: *STAKING_TOKEN_ASSET_ID,
734 };
735
736 let mut sct = tct::Tree::new();
738 for _ in 0..5 {
739 let random_note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
740 sct.insert(tct::Witness::Keep, random_note.commit())
741 .unwrap();
742 }
743 sct.insert(tct::Witness::Keep, note.commit()).unwrap();
744 sct.insert(tct::Witness::Keep, note2.commit()).unwrap();
745
746 let auth_path = sct.witness(note.commit()).unwrap();
747 let auth_path2 = sct.witness(note2.commit()).unwrap();
748
749 let plan = TransactionPlan {
752 transaction_parameters: TransactionParameters {
753 expiry_height: 0,
754 fee: Fee::default(),
755 chain_id: "".into(),
756 },
757 actions: vec![
758 SpendPlan::new(&mut OsRng, note, auth_path.position()).into(),
759 SpendPlan::new(&mut OsRng, note2, auth_path2.position()).into(),
760 OutputPlan::new(&mut OsRng, value3, test_keys::ADDRESS_1.deref().clone()).into(),
761 ],
762 detection_data: Some(DetectionDataPlan {
763 clue_plans: vec![CluePlan::new(
764 &mut OsRng,
765 test_keys::ADDRESS_1.deref().clone(),
766 1.try_into().unwrap(),
767 )],
768 }),
769 memo: None,
770 };
771
772 let transaction_view = TransactionView {
773 anchor: penumbra_sdk_tct::Root(Hash::zero()),
774 binding_sig: Signature::from([0u8; 64]),
775 body_view: TransactionBodyView {
776 action_views: plan
777 .actions
778 .iter()
779 .filter_map(|x| {
780 convert_action(&Cache::with_known_assets(), &test_keys::FULL_VIEWING_KEY, x)
781 })
782 .collect(),
783 transaction_parameters: plan.transaction_parameters.clone(),
784 detection_data: None,
785 memo_view: None,
786 },
787 };
788
789 let transaction_summary = TransactionView::summary(&transaction_view);
790
791 assert_eq!(transaction_summary.effects.len(), 2);
792 }
793
794 #[test]
795 fn test_swap_transaction_summary() {
796 let value = Value {
798 amount: 100u64.into(),
799 asset_id: *STAKING_TOKEN_ASSET_ID,
800 };
801 let note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
802
803 let value2 = Value {
804 amount: 50u64.into(),
805 asset_id: Id(Fq::rand(&mut OsRng)),
806 };
807 let note2 = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value2);
808
809 let value3 = Value {
810 amount: 75u64.into(),
811 asset_id: *STAKING_TOKEN_ASSET_ID,
812 };
813
814 let mut sct = tct::Tree::new();
816 for _ in 0..5 {
817 let random_note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
818 sct.insert(tct::Witness::Keep, random_note.commit())
819 .unwrap();
820 }
821 sct.insert(tct::Witness::Keep, note.commit()).unwrap();
822 sct.insert(tct::Witness::Keep, note2.commit()).unwrap();
823
824 let auth_path = sct.witness(note.commit()).unwrap();
825 let auth_path2 = sct.witness(note2.commit()).unwrap();
826
827 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
828 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
829 let trading_pair = TradingPair::new(gm.id(), gn.id());
830
831 let delta_1 = Amount::from(100_000u64);
832 let delta_2 = Amount::from(0u64);
833 let fee = Fee::default();
834 let claim_address: Address = test_keys::ADDRESS_0.deref().clone();
835 let plaintext = SwapPlaintext::new(
836 &mut OsRng,
837 trading_pair,
838 delta_1,
839 delta_2,
840 fee,
841 claim_address,
842 );
843
844 let plan = TransactionPlan {
847 transaction_parameters: TransactionParameters {
848 expiry_height: 0,
849 fee: Fee::default(),
850 chain_id: "".into(),
851 },
852 actions: vec![
853 SpendPlan::new(&mut OsRng, note, auth_path.position()).into(),
854 SpendPlan::new(&mut OsRng, note2, auth_path2.position()).into(),
855 OutputPlan::new(&mut OsRng, value3, test_keys::ADDRESS_1.deref().clone()).into(),
856 SwapPlan::new(&mut OsRng, plaintext.clone()).into(),
857 ],
858 detection_data: Some(DetectionDataPlan {
859 clue_plans: vec![CluePlan::new(
860 &mut OsRng,
861 test_keys::ADDRESS_1.deref().clone(),
862 1.try_into().unwrap(),
863 )],
864 }),
865 memo: None,
866 };
867
868 let transaction_view = TransactionView {
869 anchor: penumbra_sdk_tct::Root(Hash::zero()),
870 binding_sig: Signature::from([0u8; 64]),
871 body_view: TransactionBodyView {
872 action_views: plan
873 .actions
874 .iter()
875 .filter_map(|x| {
876 convert_action(&Cache::with_known_assets(), &test_keys::FULL_VIEWING_KEY, x)
877 })
878 .collect(),
879 transaction_parameters: plan.transaction_parameters.clone(),
880 detection_data: None,
881 memo_view: None,
882 },
883 };
884
885 let transaction_summary = TransactionView::summary(&transaction_view);
886
887 assert_eq!(transaction_summary.effects.len(), 2);
888 }
889}