penumbra_sdk_transaction/view/
transaction_perspective.rs1use 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#[derive(Debug, Clone, Default)]
18pub struct TransactionPerspective {
19 pub payload_keys: BTreeMap<note::StateCommitment, PayloadKey>,
34 pub spend_nullifiers: BTreeMap<Nullifier, Note>,
36 pub advice_notes: BTreeMap<note::StateCommitment, Note>,
38 pub address_views: Vec<AddressView>,
40 pub denoms: asset::Cache,
42 pub transaction_id: TransactionId,
44 pub prices: Vec<EstimatedPrice>,
46 pub extended_metadata: BTreeMap<asset::Id, Any>,
48 pub creation_transaction_ids_by_nullifier: BTreeMap<Nullifier, TransactionId>,
52 pub nullification_transaction_ids_by_commitment: BTreeMap<note::StateCommitment, TransactionId>,
56 pub batch_swap_output_data: Vec<BatchSwapOutputData>,
60 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: ¬e::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}