penumbra_sdk_transaction/view/
transaction_perspective.rs

1use anyhow::anyhow;
2use pbjson_types::Any;
3use penumbra_sdk_asset::{asset, EstimatedPrice, Value, ValueView};
4use penumbra_sdk_dex::BatchSwapOutputData;
5use penumbra_sdk_keys::{Address, AddressView, PayloadKey};
6use penumbra_sdk_proto::core::transaction::v1::{
7    self as pb, NullifierWithNote, PayloadKeyWithCommitment,
8};
9use penumbra_sdk_sct::Nullifier;
10use penumbra_sdk_shielded_pool::{note, Note, NoteView};
11use penumbra_sdk_txhash::TransactionId;
12
13use std::collections::BTreeMap;
14
15/// This represents the data to understand an individual transaction without
16/// disclosing viewing keys.
17#[derive(Debug, Clone, Default)]
18pub struct TransactionPerspective {
19    /// List of per-action payload keys. These can be used to decrypt
20    /// the notes, swaps, and memo keys in the transaction.
21    ///
22    /// One-to-one correspondence between:
23    /// * Output and note,
24    /// * Swap and note (NFT),
25    ///
26    /// There is not a one-to-one correspondence between SwapClaim and notes,
27    /// i.e. there are two notes per SwapClaim.
28    ///
29    /// For outputs, we can use the PayloadKey associated with that output
30    /// to decrypt the wrapped_memo_key, which will be used to decrypt the
31    /// memo in the transaction. This needs to be done only once, because
32    /// there is one memo shared between all outputs.
33    pub payload_keys: BTreeMap<note::StateCommitment, PayloadKey>,
34    /// Mapping of nullifiers spent in this transaction to notes.
35    pub spend_nullifiers: BTreeMap<Nullifier, Note>,
36    /// The openings of note commitments referred to in the transaction but otherwise not included in the transaction.
37    pub advice_notes: BTreeMap<note::StateCommitment, Note>,
38    /// The views of any relevant address.
39    pub address_views: Vec<AddressView>,
40    /// Any relevant denoms for viewed assets.
41    pub denoms: asset::Cache,
42    /// The transaction ID associated with this TransactionPerspective
43    pub transaction_id: TransactionId,
44    /// Any relevant estimated prices.
45    pub prices: Vec<EstimatedPrice>,
46    /// Any relevant extended metadata.
47    pub extended_metadata: BTreeMap<asset::Id, Any>,
48    /// Associates nullifiers with the transaction IDs that created the state commitments.
49    ///
50    /// Allows walking backwards from a spend to the transaction that created the note.
51    pub creation_transaction_ids_by_nullifier: BTreeMap<Nullifier, TransactionId>,
52    /// Associates commitments with the transaction IDs that eventually nullified them.
53    ///
54    /// Allows walking forwards from an output to the transaction that later spent it.
55    pub nullification_transaction_ids_by_commitment: BTreeMap<note::StateCommitment, TransactionId>,
56    /// Any relevant batch swap output data.
57    ///
58    /// This can be used to fill in information about swap outputs.
59    pub batch_swap_output_data: Vec<BatchSwapOutputData>,
60}
61
62impl TransactionPerspective {
63    pub fn view_value(&self, value: Value) -> ValueView {
64        value
65            .view_with_cache(&self.denoms)
66            .with_prices(&self.prices, &self.denoms)
67            .with_extended_metadata(self.extended_metadata.get(&value.asset_id).cloned())
68    }
69
70    pub fn view_note(&self, note: Note) -> NoteView {
71        NoteView {
72            address: self.view_address(note.address()),
73            value: self.view_value(note.value()),
74            rseed: note.rseed(),
75        }
76    }
77
78    pub fn view_address(&self, address: Address) -> AddressView {
79        match self.address_views.iter().find(|av| av.address() == address) {
80            Some(av) => av.clone(),
81            None => AddressView::Opaque { address },
82        }
83    }
84
85    pub fn get_and_view_advice_note(&self, commitment: &note::StateCommitment) -> Option<NoteView> {
86        self.advice_notes
87            .get(commitment)
88            .cloned()
89            .map(|note| self.view_note(note))
90    }
91}
92
93impl TransactionPerspective {}
94
95impl From<TransactionPerspective> for pb::TransactionPerspective {
96    fn from(msg: TransactionPerspective) -> Self {
97        let mut payload_keys = Vec::new();
98        let mut spend_nullifiers = Vec::new();
99        let mut advice_notes = Vec::new();
100        let mut address_views = Vec::new();
101        let mut denoms = Vec::new();
102
103        for (commitment, payload_key) in msg.payload_keys {
104            payload_keys.push(PayloadKeyWithCommitment {
105                payload_key: Some(payload_key.to_owned().into()),
106                commitment: Some(commitment.to_owned().into()),
107            });
108        }
109
110        for (nullifier, note) in msg.spend_nullifiers {
111            spend_nullifiers.push(NullifierWithNote {
112                nullifier: Some(nullifier.into()),
113                note: Some(note.into()),
114            })
115        }
116        for note in msg.advice_notes.into_values() {
117            advice_notes.push(note.into());
118        }
119        for address_view in msg.address_views {
120            address_views.push(address_view.into());
121        }
122        for denom in msg.denoms.values() {
123            denoms.push(denom.clone().into());
124        }
125
126        Self {
127            payload_keys,
128            spend_nullifiers,
129            advice_notes,
130            address_views,
131            denoms,
132            transaction_id: Some(msg.transaction_id.into()),
133            prices: msg.prices.into_iter().map(Into::into).collect(),
134            extended_metadata: msg
135                .extended_metadata
136                .into_iter()
137                .map(|(k, v)| pb::transaction_perspective::ExtendedMetadataById {
138                    asset_id: Some(k.into()),
139                    extended_metadata: Some(v),
140                })
141                .collect(),
142            creation_transaction_ids_by_nullifier: msg
143                .creation_transaction_ids_by_nullifier
144                .into_iter()
145                .map(
146                    |(k, v)| pb::transaction_perspective::CreationTransactionIdByNullifier {
147                        nullifier: Some(k.into()),
148                        transaction_id: Some(v.into()),
149                    },
150                )
151                .collect(),
152            nullification_transaction_ids_by_commitment: msg
153                .nullification_transaction_ids_by_commitment
154                .into_iter()
155                .map(
156                    |(k, v)| pb::transaction_perspective::NullificationTransactionIdByCommitment {
157                        commitment: Some(k.into()),
158                        transaction_id: Some(v.into()),
159                    },
160                )
161                .collect(),
162            batch_swap_output_data: msg
163                .batch_swap_output_data
164                .into_iter()
165                .map(Into::into)
166                .collect(),
167        }
168    }
169}
170
171impl TryFrom<pb::TransactionPerspective> for TransactionPerspective {
172    type Error = anyhow::Error;
173
174    fn try_from(msg: pb::TransactionPerspective) -> Result<Self, Self::Error> {
175        let mut payload_keys = BTreeMap::new();
176        let mut spend_nullifiers = BTreeMap::new();
177        let mut advice_notes = BTreeMap::new();
178        let mut address_views = Vec::new();
179        let mut denoms = BTreeMap::new();
180
181        for pk in msg.payload_keys {
182            if pk.commitment.is_some() {
183                payload_keys.insert(
184                    pk.commitment
185                        .ok_or_else(|| anyhow!("missing commitment in payload key"))?
186                        .try_into()?,
187                    pk.payload_key
188                        .ok_or_else(|| anyhow!("missing payload key"))?
189                        .try_into()?,
190                );
191            };
192        }
193
194        for nwn in msg.spend_nullifiers {
195            spend_nullifiers.insert(
196                nwn.nullifier
197                    .ok_or_else(|| anyhow!("missing nullifier in spend nullifier"))?
198                    .try_into()?,
199                nwn.note
200                    .ok_or_else(|| anyhow!("missing note in spend nullifier"))?
201                    .try_into()?,
202            );
203        }
204
205        for note in msg.advice_notes {
206            let note: Note = note.try_into()?;
207            advice_notes.insert(note.commit(), note);
208        }
209
210        for address_view in msg.address_views {
211            address_views.push(address_view.try_into()?);
212        }
213
214        for denom in msg.denoms {
215            denoms.insert(
216                denom
217                    .penumbra_asset_id
218                    .clone()
219                    .ok_or_else(|| anyhow!("missing penumbra asset ID in denom"))?
220                    .try_into()?,
221                denom.try_into()?,
222            );
223        }
224
225        let transaction_id: TransactionId = match msg.transaction_id {
226            Some(tx_id) => tx_id.try_into()?,
227            None => TransactionId::default(),
228        };
229
230        Ok(Self {
231            payload_keys,
232            spend_nullifiers,
233            advice_notes,
234            address_views,
235            denoms: denoms.try_into()?,
236            transaction_id,
237            prices: msg
238                .prices
239                .into_iter()
240                .map(TryInto::try_into)
241                .collect::<Result<_, _>>()?,
242            extended_metadata: msg
243                .extended_metadata
244                .into_iter()
245                .map(|em| {
246                    Ok((
247                        em.asset_id
248                            .ok_or_else(|| anyhow!("missing asset ID in extended metadata"))?
249                            .try_into()?,
250                        em.extended_metadata
251                            .ok_or_else(|| anyhow!("missing extended metadata"))?,
252                    ))
253                })
254                .collect::<Result<_, anyhow::Error>>()?,
255            creation_transaction_ids_by_nullifier: msg
256                .creation_transaction_ids_by_nullifier
257                .into_iter()
258                .map(|ct| {
259                    Ok((
260                        ct.nullifier
261                            .ok_or_else(|| anyhow!("missing nullifier in creation transaction ID"))?
262                            .try_into()?,
263                        ct.transaction_id
264                            .ok_or_else(|| {
265                                anyhow!("missing transaction ID in creation transaction ID")
266                            })?
267                            .try_into()?,
268                    ))
269                })
270                .collect::<Result<_, anyhow::Error>>()?,
271            nullification_transaction_ids_by_commitment: msg
272                .nullification_transaction_ids_by_commitment
273                .into_iter()
274                .map(|nt| {
275                    Ok((
276                        nt.commitment
277                            .ok_or_else(|| {
278                                anyhow!("missing commitment in nullification transaction ID")
279                            })?
280                            .try_into()?,
281                        nt.transaction_id
282                            .ok_or_else(|| {
283                                anyhow!("missing transaction ID in nullification transaction ID")
284                            })?
285                            .try_into()?,
286                    ))
287                })
288                .collect::<Result<_, anyhow::Error>>()?,
289            batch_swap_output_data: msg
290                .batch_swap_output_data
291                .into_iter()
292                .map(TryInto::try_into)
293                .collect::<Result<_, _>>()?,
294        })
295    }
296}