penumbra_sdk_transaction/
is_action.rs1use 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
32pub trait IsAction {
37 fn balance_commitment(&self) -> balance::Commitment;
38 fn view_from_perspective(&self, txp: &TransactionPerspective) -> ActionView;
39}
40
41impl 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 let output_view = if let Some(payload_key) = txp.payload_keys.get(¬e_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 OutputView::Visible {
136 output: self.to_owned(),
137 note: txp.view_note(decrypted_note),
138 payload_key: decrypted_memo_key,
139 }
140 } else {
141 OutputView::Opaque {
143 output: self.to_owned(),
144 }
145 }
146 } else {
147 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 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 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 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 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 let bsod = txp
354 .batch_swap_output_data
355 .iter()
356 .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 let bsod = txp
392 .batch_swap_output_data
393 .iter()
394 .find(|bsod| bsod.trading_pair == self.body.trading_pair);
397
398 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 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 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 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}