penumbra_sdk_transaction/
view.rs

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    /// Acts as a higher-order translator that summarizes a TransactionSummary by consolidating
102    /// effects for each unique address.
103    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    /// Produces a TransactionSummary, iterating through each visible action and collecting the effects of the transaction.
118    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                        // Provided imbalance (+)
126                        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                        // Required imbalance (-)
141                        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                        // Required imbalance (-)
178                        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                        // Provided imbalance (+)
211                        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                _ => {} // Fill in other action views as necessary
219            }
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        // Iterate through the detection_data vec, and convert each FMD clue.
308        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        // Generate two notes controlled by the test address.
715        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        // Record that note in an SCT, where we can generate an auth path.
733        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        // Add a single spend and output to the transaction plan such that the
746        // transaction balances.
747        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        // Generate two notes controlled by the test address.
793        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        // Record that note in an SCT, where we can generate an auth path.
811        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        // Add a single spend and output to the transaction plan such that the
841        // transaction balances.
842        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}