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