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 = 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                        // Required imbalance (-)
143                        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                        // Required imbalance (-)
182                        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                        // Provided imbalance (+)
215                        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                _ => {} // Fill in other action views as neccessary
223            }
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        // Iterate through the detection_data vec, and convert each FMD clue.
312        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        // Generate two notes controlled by the test address.
719        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        // Record that note in an SCT, where we can generate an auth path.
737        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        // Add a single spend and output to the transaction plan such that the
750        // transaction balances.
751        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        // Generate two notes controlled by the test address.
797        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        // Record that note in an SCT, where we can generate an auth path.
815        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        // Add a single spend and output to the transaction plan such that the
845        // transaction balances.
846        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}