penumbra_sdk_proto/
event.rs

1use crate::{DomainType, Message, Name};
2use anyhow::{self, Context};
3use serde::{de::DeserializeOwned, Serialize};
4use std::collections::HashMap;
5use tendermint::abci::{self, EventAttribute};
6
7pub trait ProtoEvent: Message + Name + Serialize + DeserializeOwned + Sized {
8    fn into_event(&self) -> abci::Event {
9        let kind = Self::full_name();
10
11        let event_json = serde_json::to_value(&self)
12            .expect("ProtoEvent constrained values should be JSON serializable.");
13
14        // WARNING: Assuming that Rust value will always serialize into a valid JSON Object value. This falls apart the moment that isn't true, so we fail hard if that turns out to be the case.
15        let mut attributes: Vec<EventAttribute> = event_json
16            .as_object()
17            .expect("serde_json Serialized ProtoEvent should not be empty.")
18            .into_iter()
19            .map(|(key, v)| {
20                abci::EventAttribute::V037(abci::v0_37::EventAttribute {
21                    value: serde_json::to_string(v)
22                        .expect("must be able to serialize value as JSON"),
23                    key: key.to_string(),
24                    index: true,
25                })
26            })
27            .collect();
28
29        // NOTE: cosmo-sdk sorts the attribute list so that it's deterministic every time.[0] I don't know if that is actually conformant but continuing that pattern here for now.
30        // [0]: https://github.com/cosmos/cosmos-sdk/blob/8fb62054c59e580c0ae0c898751f8dc46044499a/types/events.go#L102-L104
31        attributes.sort_by(|a, b| (&a.key_bytes()).cmp(&b.key_bytes()));
32
33        return abci::Event::new(kind, attributes);
34    }
35
36    fn from_event(event: &abci::Event) -> anyhow::Result<Self> {
37        // Check that we're dealing with the right type of event.
38        if Self::full_name() != event.kind {
39            return Err(anyhow::anyhow!(format!(
40                "ABCI Event {} not expected for {}",
41                event.kind,
42                Self::full_name()
43            )));
44        }
45
46        // NOTE: Is there any condition where there would be duplicate EventAttributes and problems that fall out of that?
47        let mut attributes = HashMap::<String, serde_json::Value>::new();
48        for attr in &event.attributes {
49            let value = serde_json::from_slice(attr.value_bytes())
50                .with_context(|| format!("could not parse JSON for attribute {:?}", attr))?;
51            attributes.insert(String::from_utf8_lossy(attr.key_bytes()).into(), value);
52        }
53
54        let json = serde_json::to_value(attributes)
55            .expect("HashMap of String, serde_json::Value should be serializeable.");
56
57        return Ok(
58            serde_json::from_value(json).context("could not deserialise ProtoJSON into event")?
59        );
60    }
61}
62
63impl<E: Message + Name + Serialize + DeserializeOwned + Sized> ProtoEvent for E {}
64
65#[cfg(test)]
66mod tests {
67    #[test]
68    fn event_round_trip() {
69        use super::*;
70        use crate::core::component::sct::v1::Nullifier;
71        use crate::core::component::shielded_pool::v1::{EventOutput, EventSpend};
72        use crate::crypto::tct::v1::StateCommitment;
73
74        let proto_spend = EventSpend {
75            nullifier: Some(Nullifier {
76                inner: vec![
77                    148, 190, 149, 23, 86, 113, 152, 145, 104, 242, 142, 162, 233, 239, 137, 141,
78                    140, 164, 180, 98, 154, 55, 168, 255, 163, 228, 179, 176, 26, 25, 219, 211,
79                ],
80            }),
81        };
82
83        let abci_spend = proto_spend.into_event();
84
85        let expected_abci_spend = abci::Event::new(
86            "penumbra.core.component.shielded_pool.v1.EventSpend",
87            [abci::EventAttribute::V037(abci::v0_37::EventAttribute {
88                key: "nullifier".to_string(),
89                value: "{\"inner\":\"lL6VF1ZxmJFo8o6i6e+JjYyktGKaN6j/o+SzsBoZ29M=\"}".to_string(),
90                index: true,
91            })],
92        );
93        assert_eq!(abci_spend, expected_abci_spend);
94
95        let proto_spend2 = EventSpend::from_event(&abci_spend).unwrap();
96
97        assert_eq!(proto_spend, proto_spend2);
98
99        let proto_output = EventOutput {
100            // This is the same bytes as the nullifier above, we just care about the data format, not the value.
101            note_commitment: Some(StateCommitment {
102                inner: vec![
103                    148, 190, 149, 23, 86, 113, 152, 145, 104, 242, 142, 162, 233, 239, 137, 141,
104                    140, 164, 180, 98, 154, 55, 168, 255, 163, 228, 179, 176, 26, 25, 219, 211,
105                ],
106            }),
107        };
108
109        let abci_output = proto_output.into_event();
110
111        let expected_abci_output = abci::Event::new(
112            "penumbra.core.component.shielded_pool.v1.EventOutput",
113            [abci::EventAttribute::V037(abci::v0_37::EventAttribute {
114                // note: attribute keys become camelCase because ProtoJSON...
115                key: "noteCommitment".to_string(),
116                // note: attribute values are JSON objects, potentially nested as here
117                value: "{\"inner\":\"lL6VF1ZxmJFo8o6i6e+JjYyktGKaN6j/o+SzsBoZ29M=\"}".to_string(),
118                index: true,
119            })],
120        );
121        assert_eq!(abci_output, expected_abci_output);
122
123        let proto_output2 = EventOutput::from_event(&abci_output).unwrap();
124        assert_eq!(proto_output, proto_output2);
125    }
126}
127
128/// An extension trait allowing for easy conversion from events into domain types.
129///
130/// This makes the task of writing code that processes events much more easy,
131/// since you can just attempt to parse the event directly into the specific domain
132/// type.
133pub trait EventDomainType: DomainType
134where
135    <Self as DomainType>::Proto: ProtoEvent,
136    anyhow::Error: From<<Self as TryFrom<<Self as DomainType>::Proto>>::Error>,
137{
138    fn try_from_event(event: &abci::Event) -> anyhow::Result<Self> {
139        Ok(<Self as DomainType>::Proto::from_event(event)?.try_into()?)
140    }
141}
142
143impl<T: DomainType> EventDomainType for T
144where
145    <T as DomainType>::Proto: ProtoEvent,
146    anyhow::Error: From<<Self as TryFrom<<Self as DomainType>::Proto>>::Error>,
147{
148}