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, PositionMetadataKey};
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    /// The key used to decrypt position metadata.
61    ///
62    /// We leave this as optional for maximal backwards compatability.
63    pub position_metadata_key: Option<PositionMetadataKey>,
64}
65
66impl TransactionPerspective {
67    pub fn view_value(&self, value: Value) -> ValueView {
68        value
69            .view_with_cache(&self.denoms)
70            .with_prices(&self.prices, &self.denoms)
71            .with_extended_metadata(self.extended_metadata.get(&value.asset_id).cloned())
72    }
73
74    pub fn view_note(&self, note: Note) -> NoteView {
75        NoteView {
76            address: self.view_address(note.address()),
77            value: self.view_value(note.value()),
78            rseed: note.rseed(),
79        }
80    }
81
82    pub fn view_address(&self, address: Address) -> AddressView {
83        match self.address_views.iter().find(|av| av.address() == address) {
84            Some(av) => av.clone(),
85            None => AddressView::Opaque { address },
86        }
87    }
88
89    pub fn get_and_view_advice_note(&self, commitment: &note::StateCommitment) -> Option<NoteView> {
90        self.advice_notes
91            .get(commitment)
92            .cloned()
93            .map(|note| self.view_note(note))
94    }
95}
96
97impl TransactionPerspective {}
98
99impl From<TransactionPerspective> for pb::TransactionPerspective {
100    fn from(msg: TransactionPerspective) -> Self {
101        let mut payload_keys = Vec::new();
102        let mut spend_nullifiers = Vec::new();
103        let mut advice_notes = Vec::new();
104        let mut address_views = Vec::new();
105        let mut denoms = Vec::new();
106
107        for (commitment, payload_key) in msg.payload_keys {
108            payload_keys.push(PayloadKeyWithCommitment {
109                payload_key: Some(payload_key.to_owned().into()),
110                commitment: Some(commitment.to_owned().into()),
111            });
112        }
113
114        for (nullifier, note) in msg.spend_nullifiers {
115            spend_nullifiers.push(NullifierWithNote {
116                nullifier: Some(nullifier.into()),
117                note: Some(note.into()),
118            })
119        }
120        for note in msg.advice_notes.into_values() {
121            advice_notes.push(note.into());
122        }
123        for address_view in msg.address_views {
124            address_views.push(address_view.into());
125        }
126        for denom in msg.denoms.values() {
127            denoms.push(denom.clone().into());
128        }
129
130        Self {
131            payload_keys,
132            spend_nullifiers,
133            advice_notes,
134            address_views,
135            denoms,
136            transaction_id: Some(msg.transaction_id.into()),
137            prices: msg.prices.into_iter().map(Into::into).collect(),
138            extended_metadata: msg
139                .extended_metadata
140                .into_iter()
141                .map(|(k, v)| pb::transaction_perspective::ExtendedMetadataById {
142                    asset_id: Some(k.into()),
143                    extended_metadata: Some(v),
144                })
145                .collect(),
146            creation_transaction_ids_by_nullifier: msg
147                .creation_transaction_ids_by_nullifier
148                .into_iter()
149                .map(
150                    |(k, v)| pb::transaction_perspective::CreationTransactionIdByNullifier {
151                        nullifier: Some(k.into()),
152                        transaction_id: Some(v.into()),
153                    },
154                )
155                .collect(),
156            nullification_transaction_ids_by_commitment: msg
157                .nullification_transaction_ids_by_commitment
158                .into_iter()
159                .map(
160                    |(k, v)| pb::transaction_perspective::NullificationTransactionIdByCommitment {
161                        commitment: Some(k.into()),
162                        transaction_id: Some(v.into()),
163                    },
164                )
165                .collect(),
166            batch_swap_output_data: msg
167                .batch_swap_output_data
168                .into_iter()
169                .map(Into::into)
170                .collect(),
171            position_metadata_key: msg.position_metadata_key.map(|x| x.into()),
172        }
173    }
174}
175
176impl TryFrom<pb::TransactionPerspective> for TransactionPerspective {
177    type Error = anyhow::Error;
178
179    fn try_from(msg: pb::TransactionPerspective) -> Result<Self, Self::Error> {
180        let mut payload_keys = BTreeMap::new();
181        let mut spend_nullifiers = BTreeMap::new();
182        let mut advice_notes = BTreeMap::new();
183        let mut address_views = Vec::new();
184        let mut denoms = BTreeMap::new();
185
186        for pk in msg.payload_keys {
187            if pk.commitment.is_some() {
188                payload_keys.insert(
189                    pk.commitment
190                        .ok_or_else(|| anyhow!("missing commitment in payload key"))?
191                        .try_into()?,
192                    pk.payload_key
193                        .ok_or_else(|| anyhow!("missing payload key"))?
194                        .try_into()?,
195                );
196            };
197        }
198
199        for nwn in msg.spend_nullifiers {
200            spend_nullifiers.insert(
201                nwn.nullifier
202                    .ok_or_else(|| anyhow!("missing nullifier in spend nullifier"))?
203                    .try_into()?,
204                nwn.note
205                    .ok_or_else(|| anyhow!("missing note in spend nullifier"))?
206                    .try_into()?,
207            );
208        }
209
210        for note in msg.advice_notes {
211            let note: Note = note.try_into()?;
212            advice_notes.insert(note.commit(), note);
213        }
214
215        for address_view in msg.address_views {
216            address_views.push(address_view.try_into()?);
217        }
218
219        for denom in msg.denoms {
220            denoms.insert(
221                denom
222                    .penumbra_asset_id
223                    .clone()
224                    .ok_or_else(|| anyhow!("missing penumbra asset ID in denom"))?
225                    .try_into()?,
226                denom.try_into()?,
227            );
228        }
229
230        let transaction_id: TransactionId = match msg.transaction_id {
231            Some(tx_id) => tx_id.try_into()?,
232            None => TransactionId::default(),
233        };
234
235        Ok(Self {
236            payload_keys,
237            spend_nullifiers,
238            advice_notes,
239            address_views,
240            denoms: denoms.try_into()?,
241            transaction_id,
242            prices: msg
243                .prices
244                .into_iter()
245                .map(TryInto::try_into)
246                .collect::<Result<_, _>>()?,
247            extended_metadata: msg
248                .extended_metadata
249                .into_iter()
250                .map(|em| {
251                    Ok((
252                        em.asset_id
253                            .ok_or_else(|| anyhow!("missing asset ID in extended metadata"))?
254                            .try_into()?,
255                        em.extended_metadata
256                            .ok_or_else(|| anyhow!("missing extended metadata"))?,
257                    ))
258                })
259                .collect::<Result<_, anyhow::Error>>()?,
260            creation_transaction_ids_by_nullifier: msg
261                .creation_transaction_ids_by_nullifier
262                .into_iter()
263                .map(|ct| {
264                    Ok((
265                        ct.nullifier
266                            .ok_or_else(|| anyhow!("missing nullifier in creation transaction ID"))?
267                            .try_into()?,
268                        ct.transaction_id
269                            .ok_or_else(|| {
270                                anyhow!("missing transaction ID in creation transaction ID")
271                            })?
272                            .try_into()?,
273                    ))
274                })
275                .collect::<Result<_, anyhow::Error>>()?,
276            nullification_transaction_ids_by_commitment: msg
277                .nullification_transaction_ids_by_commitment
278                .into_iter()
279                .map(|nt| {
280                    Ok((
281                        nt.commitment
282                            .ok_or_else(|| {
283                                anyhow!("missing commitment in nullification transaction ID")
284                            })?
285                            .try_into()?,
286                        nt.transaction_id
287                            .ok_or_else(|| {
288                                anyhow!("missing transaction ID in nullification transaction ID")
289                            })?
290                            .try_into()?,
291                    ))
292                })
293                .collect::<Result<_, anyhow::Error>>()?,
294            batch_swap_output_data: msg
295                .batch_swap_output_data
296                .into_iter()
297                .map(TryInto::try_into)
298                .collect::<Result<_, _>>()?,
299            position_metadata_key: msg
300                .position_metadata_key
301                .map(|x| x.try_into())
302                .transpose()?,
303        })
304    }
305}