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