penumbra_sdk_transaction/
is_action.rs

1use ark_ff::Zero;
2use decaf377::Fr;
3use penumbra_sdk_asset::{balance, Value};
4use penumbra_sdk_auction::auction::dutch::actions::{
5    view::{ActionDutchAuctionScheduleView, ActionDutchAuctionWithdrawView},
6    ActionDutchAuctionEnd, ActionDutchAuctionSchedule, ActionDutchAuctionWithdraw,
7};
8use penumbra_sdk_community_pool::{CommunityPoolDeposit, CommunityPoolOutput, CommunityPoolSpend};
9use penumbra_sdk_dex::{
10    lp::{
11        action::{PositionClose, PositionOpen, PositionWithdraw},
12        position,
13        view::PositionOpenView,
14        LpNft, PositionMetadata,
15    },
16    swap::{Swap, SwapCiphertext, SwapView},
17    swap_claim::{SwapClaim, SwapClaimView},
18};
19use penumbra_sdk_funding::liquidity_tournament::{
20    ActionLiquidityTournamentVote, ActionLiquidityTournamentVoteView,
21};
22use penumbra_sdk_governance::{
23    DelegatorVote, DelegatorVoteView, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw,
24    ValidatorVote, VotingReceiptToken,
25};
26use penumbra_sdk_ibc::IbcRelay;
27use penumbra_sdk_shielded_pool::{Ics20Withdrawal, Note, Output, OutputView, Spend, SpendView};
28use penumbra_sdk_stake::{Delegate, Undelegate, UndelegateClaim};
29
30use crate::{Action, ActionView, TransactionPerspective};
31
32// TODO: how do we have this be implemented in the component crates?
33// currently can't because of txp
34
35/// Common behavior between Penumbra actions.
36pub trait IsAction {
37    fn balance_commitment(&self) -> balance::Commitment;
38    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView;
39}
40
41// foreign types
42
43impl From<DelegatorVote> for Action {
44    fn from(value: DelegatorVote) -> Self {
45        Action::DelegatorVote(value)
46    }
47}
48
49impl IsAction for DelegatorVote {
50    fn balance_commitment(&self) -> balance::Commitment {
51        Value {
52            amount: self.body.unbonded_amount,
53            asset_id: VotingReceiptToken::new(self.body.proposal).id(),
54        }
55        .commit(Fr::zero())
56    }
57
58    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
59        let delegator_vote_view = match txp.spend_nullifiers.get(&self.body.nullifier) {
60            Some(note) => DelegatorVoteView::Visible {
61                delegator_vote: self.to_owned(),
62                note: txp.view_note(note.to_owned()),
63            },
64            None => DelegatorVoteView::Opaque {
65                delegator_vote: self.to_owned(),
66            },
67        };
68
69        ActionView::DelegatorVote(delegator_vote_view)
70    }
71}
72
73impl IsAction for ProposalDepositClaim {
74    fn balance_commitment(&self) -> balance::Commitment {
75        self.balance().commit(Fr::zero())
76    }
77
78    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
79        ActionView::ProposalDepositClaim(self.clone())
80    }
81}
82
83impl IsAction for ProposalSubmit {
84    fn balance_commitment(&self) -> balance::Commitment {
85        self.balance().commit(Fr::zero())
86    }
87
88    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
89        ActionView::ProposalSubmit(self.to_owned())
90    }
91}
92
93impl IsAction for ProposalWithdraw {
94    fn balance_commitment(&self) -> balance::Commitment {
95        self.balance().commit(Fr::zero())
96    }
97
98    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
99        ActionView::ProposalWithdraw(self.to_owned())
100    }
101}
102
103impl IsAction for ValidatorVote {
104    fn balance_commitment(&self) -> balance::Commitment {
105        Default::default()
106    }
107
108    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
109        ActionView::ValidatorVote(self.to_owned())
110    }
111}
112
113impl IsAction for Output {
114    fn balance_commitment(&self) -> balance::Commitment {
115        self.body.balance_commitment
116    }
117
118    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
119        let note_commitment = self.body.note_payload.note_commitment;
120        let epk = self.body.note_payload.ephemeral_key;
121        // Retrieve payload key for associated note commitment
122        let output_view = if let Some(payload_key) = txp.payload_keys.get(&note_commitment) {
123            let decrypted_note = Note::decrypt_with_payload_key(
124                &self.body.note_payload.encrypted_note,
125                payload_key,
126                &epk,
127            );
128
129            let decrypted_memo_key = self.body.wrapped_memo_key.decrypt_outgoing(payload_key);
130
131            if let (Ok(decrypted_note), Ok(decrypted_memo_key)) =
132                (decrypted_note, decrypted_memo_key)
133            {
134                // Neither decryption failed, so return the visible ActionView
135                OutputView::Visible {
136                    output: self.to_owned(),
137                    note: txp.view_note(decrypted_note),
138                    payload_key: decrypted_memo_key,
139                }
140            } else {
141                // One or both of the note or memo key is missing, so return the opaque ActionView
142                OutputView::Opaque {
143                    output: self.to_owned(),
144                }
145            }
146        } else {
147            // There was no payload key found, so return the opaque ActionView
148            OutputView::Opaque {
149                output: self.to_owned(),
150            }
151        };
152
153        ActionView::Output(output_view)
154    }
155}
156
157impl IsAction for Spend {
158    fn balance_commitment(&self) -> balance::Commitment {
159        self.body.balance_commitment
160    }
161
162    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
163        let spend_view = match txp.spend_nullifiers.get(&self.body.nullifier) {
164            Some(note) => SpendView::Visible {
165                spend: self.to_owned(),
166                note: txp.view_note(note.to_owned()),
167            },
168            None => SpendView::Opaque {
169                spend: self.to_owned(),
170            },
171        };
172
173        ActionView::Spend(spend_view)
174    }
175}
176
177impl IsAction for Delegate {
178    fn balance_commitment(&self) -> balance::Commitment {
179        self.balance().commit(Fr::zero())
180    }
181
182    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
183        ActionView::Delegate(self.to_owned())
184    }
185}
186
187impl IsAction for Undelegate {
188    fn balance_commitment(&self) -> balance::Commitment {
189        self.balance().commit(Fr::zero())
190    }
191
192    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
193        ActionView::Undelegate(self.to_owned())
194    }
195}
196
197impl IsAction for UndelegateClaim {
198    fn balance_commitment(&self) -> balance::Commitment {
199        self.body.balance_commitment
200    }
201
202    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
203        ActionView::UndelegateClaim(self.to_owned())
204    }
205}
206
207impl IsAction for IbcRelay {
208    fn balance_commitment(&self) -> balance::Commitment {
209        Default::default()
210    }
211
212    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
213        ActionView::IbcRelay(self.clone())
214    }
215}
216
217impl IsAction for Ics20Withdrawal {
218    fn balance_commitment(&self) -> balance::Commitment {
219        self.balance().commit(Fr::zero())
220    }
221
222    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
223        ActionView::Ics20Withdrawal(self.to_owned())
224    }
225}
226
227impl IsAction for CommunityPoolDeposit {
228    fn balance_commitment(&self) -> balance::Commitment {
229        self.balance().commit(Fr::zero())
230    }
231
232    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
233        ActionView::CommunityPoolDeposit(self.clone())
234    }
235}
236
237impl IsAction for CommunityPoolOutput {
238    fn balance_commitment(&self) -> balance::Commitment {
239        // Outputs from the Community Pool require value
240        self.balance().commit(Fr::zero())
241    }
242
243    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
244        ActionView::CommunityPoolOutput(self.clone())
245    }
246}
247
248impl IsAction for CommunityPoolSpend {
249    fn balance_commitment(&self) -> balance::Commitment {
250        self.balance().commit(Fr::zero())
251    }
252
253    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
254        ActionView::CommunityPoolSpend(self.clone())
255    }
256}
257
258impl IsAction for PositionOpen {
259    fn balance_commitment(&self) -> balance::Commitment {
260        self.balance().commit(Fr::zero())
261    }
262
263    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
264        let view = match txp.position_metadata_key.and_then(|key| {
265            PositionMetadata::decrypt(&key, self.encrypted_metadata.as_ref().map(|x| x.as_slice()))
266                .ok()
267                .flatten()
268        }) {
269            None => PositionOpenView::Opaque {
270                action: self.clone(),
271            },
272            Some(metadata) => PositionOpenView::Visible {
273                action: self.clone(),
274                metadata,
275            },
276        };
277        ActionView::PositionOpen(view)
278    }
279}
280
281impl IsAction for PositionClose {
282    fn balance_commitment(&self) -> balance::Commitment {
283        self.balance().commit(Fr::zero())
284    }
285
286    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
287        ActionView::PositionClose(self.to_owned())
288    }
289}
290
291impl IsAction for PositionWithdraw {
292    fn balance_commitment(&self) -> balance::Commitment {
293        let prev_state_nft = if self.sequence == 0 {
294            Value {
295                amount: 1u64.into(),
296                asset_id: LpNft::new(self.position_id, position::State::Closed).asset_id(),
297            }
298        } else {
299            Value {
300                amount: 1u64.into(),
301                asset_id: LpNft::new(
302                    self.position_id,
303                    position::State::Withdrawn {
304                        sequence: self.sequence - 1,
305                    },
306                )
307                .asset_id(),
308            }
309        }
310        .commit(Fr::zero());
311
312        let next_state_nft = Value {
313            amount: 1u64.into(),
314            asset_id: LpNft::new(
315                self.position_id,
316                position::State::Withdrawn {
317                    sequence: self.sequence,
318                },
319            )
320            .asset_id(),
321        }
322        .commit(Fr::zero());
323
324        // The action consumes a closed position and produces the position's reserves and a withdrawn position NFT.
325        self.reserves_commitment - prev_state_nft + next_state_nft
326    }
327
328    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
329        ActionView::PositionWithdraw(self.to_owned())
330    }
331}
332
333impl IsAction for Swap {
334    /// Compute a commitment to the value contributed to a transaction by this swap.
335    /// Will subtract (v1,t1), (v2,t2), and (f,fee_token)
336    fn balance_commitment(&self) -> balance::Commitment {
337        self.balance_commitment_inner()
338    }
339
340    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
341        let commitment = self.body.payload.commitment;
342
343        let plaintext = txp.payload_keys.get(&commitment).and_then(|payload_key| {
344            // Decrypt swap ciphertext
345            SwapCiphertext::decrypt_with_payload_key(&self.body.payload.encrypted_swap, payload_key)
346                .ok()
347        });
348
349        ActionView::Swap(match plaintext {
350            Some(swap_plaintext) => {
351                // If we can find a matching BSOD in the TxP, use it to compute the output notes
352                // for the swap.
353                let bsod = txp
354                    .batch_swap_output_data
355                    .iter()
356                    // This finds the first matching one; there should only be one
357                    // per trading pair per block and we trust the TxP provider not to lie about it.
358                    .find(|bsod| bsod.trading_pair == swap_plaintext.trading_pair);
359
360                let (output_1, output_2) = match bsod.map(|bsod| swap_plaintext.output_notes(bsod))
361                {
362                    Some((output_1, output_2)) => {
363                        (Some(txp.view_note(output_1)), Some(txp.view_note(output_2)))
364                    }
365                    None => (None, None),
366                };
367
368                SwapView::Visible {
369                    swap: self.to_owned(),
370                    swap_plaintext: swap_plaintext.clone(),
371                    output_1,
372                    output_2,
373                    claim_tx: txp
374                        .nullification_transaction_ids_by_commitment
375                        .get(&commitment)
376                        .cloned(),
377                    batch_swap_output_data: bsod.cloned(),
378                    asset_1_metadata: txp
379                        .denoms
380                        .get(&swap_plaintext.trading_pair.asset_1())
381                        .cloned(),
382                    asset_2_metadata: txp
383                        .denoms
384                        .get(&swap_plaintext.trading_pair.asset_2())
385                        .cloned(),
386                }
387            }
388            None => {
389                // If we can find a matching BSOD in the TxP, we can use it to compute the output notes
390                // for the swap.
391                let bsod = txp
392                    .batch_swap_output_data
393                    .iter()
394                    // This finds the first matching one; there should only be one
395                    // per trading pair per block and we trust the TxP provider not to lie about it.
396                    .find(|bsod| bsod.trading_pair == self.body.trading_pair);
397
398                // We can get the denom metadata whether we get a BSOD or not
399                let denom_1 = txp.denoms.get(&self.body.trading_pair.asset_1()).cloned();
400                let denom_2 = txp.denoms.get(&self.body.trading_pair.asset_2()).cloned();
401
402                match bsod {
403                    None => {
404                        // If we can't find a matching BSOD, we can't compute the output notes
405                        // for the swap.
406                        SwapView::Opaque {
407                            swap: self.to_owned(),
408                            batch_swap_output_data: None,
409                            output_1: None,
410                            output_2: None,
411                            asset_1_metadata: denom_1.clone(),
412                            asset_2_metadata: denom_2.clone(),
413                        }
414                    }
415                    Some(bsod) => {
416                        // If we can find a matching BSOD, use it to compute the output notes
417                        // for the swap.
418                        let (lambda_1_i, lambda_2_i) =
419                            bsod.pro_rata_outputs((self.body.delta_1_i, self.body.delta_2_i));
420                        SwapView::Opaque {
421                            swap: self.to_owned(),
422                            batch_swap_output_data: Some(bsod.clone()),
423                            asset_1_metadata: denom_1.clone(),
424                            asset_2_metadata: denom_2.clone(),
425                            output_1: Some(
426                                Value {
427                                    amount: lambda_1_i,
428                                    asset_id: self.body.trading_pair.asset_1(),
429                                }
430                                .view_with_cache(&txp.denoms),
431                            ),
432                            output_2: Some(
433                                Value {
434                                    amount: lambda_2_i,
435                                    asset_id: self.body.trading_pair.asset_2(),
436                                }
437                                .view_with_cache(&txp.denoms),
438                            ),
439                        }
440                    }
441                }
442            }
443        })
444    }
445}
446
447impl IsAction for SwapClaim {
448    fn balance_commitment(&self) -> balance::Commitment {
449        self.balance().commit(Fr::zero())
450    }
451
452    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
453        // Get the advice notes for each output from the swap claim
454        let output_1 = txp.advice_notes.get(&self.body.output_1_commitment);
455        let output_2 = txp.advice_notes.get(&self.body.output_2_commitment);
456
457        match (output_1, output_2) {
458            (Some(output_1), Some(output_2)) => {
459                let swap_claim_view = SwapClaimView::Visible {
460                    swap_claim: self.to_owned(),
461                    output_1: txp.view_note(output_1.to_owned()),
462                    output_2: txp.view_note(output_2.to_owned()),
463                    swap_tx: txp
464                        .creation_transaction_ids_by_nullifier
465                        .get(&self.body.nullifier)
466                        .cloned(),
467                };
468                ActionView::SwapClaim(swap_claim_view)
469            }
470            _ => {
471                let swap_claim_view = SwapClaimView::Opaque {
472                    swap_claim: self.to_owned(),
473                };
474                ActionView::SwapClaim(swap_claim_view)
475            }
476        }
477    }
478}
479
480impl IsAction for ActionDutchAuctionSchedule {
481    fn balance_commitment(&self) -> balance::Commitment {
482        self.balance().commit(Fr::zero())
483    }
484
485    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
486        let view = ActionDutchAuctionScheduleView {
487            action: self.to_owned(),
488            auction_id: self.description.id(),
489            input_metadata: txp.denoms.get_by_id(self.description.input.asset_id),
490            output_metadata: txp.denoms.get_by_id(self.description.output_id),
491        };
492        ActionView::ActionDutchAuctionSchedule(view)
493    }
494}
495
496impl IsAction for ActionDutchAuctionEnd {
497    fn balance_commitment(&self) -> balance::Commitment {
498        self.balance().commit(Fr::zero())
499    }
500
501    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
502        ActionView::ActionDutchAuctionEnd(self.to_owned())
503    }
504}
505
506impl IsAction for ActionDutchAuctionWithdraw {
507    fn balance_commitment(&self) -> balance::Commitment {
508        self.balance_commitment()
509    }
510
511    fn view_from_perspective(&self, _txp: &TransactionPerspective) -> ActionView {
512        let view = ActionDutchAuctionWithdrawView {
513            action: self.to_owned(),
514            reserves: vec![],
515        };
516        ActionView::ActionDutchAuctionWithdraw(view)
517    }
518}
519
520impl IsAction for ActionLiquidityTournamentVote {
521    fn balance_commitment(&self) -> balance::Commitment {
522        self.balance_commitment()
523    }
524
525    fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView {
526        let lqt_vote_view = match txp.spend_nullifiers.get(&self.body.nullifier) {
527            Some(note) => ActionLiquidityTournamentVoteView::Visible {
528                vote: self.to_owned(),
529                note: txp.view_note(note.to_owned()),
530            },
531            None => ActionLiquidityTournamentVoteView::Opaque {
532                vote: self.to_owned(),
533            },
534        };
535
536        ActionView::ActionLiquidityTournamentVote(lqt_vote_view)
537    }
538}