Skip to main content

nexus_sdk/events/
mod.rs

1use {
2    crate::sui,
3    anyhow::{bail, Result},
4    serde::{Deserialize, Serialize},
5    thiserror::Error,
6};
7
8mod parsing;
9mod query;
10
11pub use query::*;
12
13/// Nexus event whose datatype is absent from [`NexusEventKind`].
14#[derive(Clone, Debug, Error)]
15#[error("Nexus event '{event_type}' from source package '{source_package}' is unsupported")]
16pub struct UnsupportedNexusEvent {
17    /// Transaction digest and event sequence that identify the event.
18    pub id: (sui::types::Digest, u64),
19    /// Package recorded by Sui as the source of the Move command.
20    pub source_package: sui::types::Address,
21    /// Full inner event type. Its address identifies the definition package.
22    pub event_type: Box<sui::types::StructTag>,
23}
24
25/// Failure returned while converting a Sui event into a [`NexusEvent`].
26#[derive(Debug, Error)]
27pub enum NexusEventDecodeError {
28    /// Required event identity is missing or invalid.
29    #[error("Nexus event identity is invalid: {0}")]
30    Identity(String),
31    /// A required Sui event field is missing.
32    #[error("Required Nexus event field is missing: {0}")]
33    MissingField(&'static str),
34    /// The Sui event type is invalid.
35    #[error("Nexus event type is invalid: {0}")]
36    EventType(#[from] sui::types::TypeParseError),
37    /// The immutable package graph selected by the emitter is invalid.
38    #[error("Could not resolve Nexus event emitter package '{package}': {source}")]
39    EmitterPackage {
40        /// Package recorded as the event emitter.
41        package: sui::types::Address,
42        /// Resolver failure for that package.
43        #[source]
44        source: crate::nexus::error::NexusError,
45    },
46    /// The inner datatype belongs to Nexus but is absent from the SDK contract.
47    #[error(transparent)]
48    UnsupportedEvent(#[from] UnsupportedNexusEvent),
49    /// The Nexus event contents are invalid.
50    #[error("Nexus event contents are invalid: {0}")]
51    Contents(#[source] anyhow::Error),
52}
53
54fn deserialize_u64_to_datetime<'de, D>(
55    deserializer: D,
56) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
57where
58    D: serde::Deserializer<'de>,
59{
60    let timestamp = u64::deserialize(deserializer)?;
61    chrono::DateTime::from_timestamp_millis(timestamp as i64)
62        .ok_or_else(|| serde::de::Error::custom("datetime out of range"))
63}
64
65fn serialize_datetime_to_u64<S>(
66    value: &chrono::DateTime<chrono::Utc>,
67    serializer: S,
68) -> Result<S::Ok, S::Error>
69where
70    S: serde::Serializer,
71{
72    serializer.serialize_u64(value.timestamp_millis() as u64)
73}
74
75fn deserialize_u64_to_duration<'de, D>(deserializer: D) -> Result<chrono::Duration, D::Error>
76where
77    D: serde::Deserializer<'de>,
78{
79    let millis = u64::deserialize(deserializer)?;
80    Ok(chrono::Duration::milliseconds(millis as i64))
81}
82
83fn serialize_duration_to_u64<S>(value: &chrono::Duration, serializer: S) -> Result<S::Ok, S::Error>
84where
85    S: serde::Serializer,
86{
87    serializer.serialize_u64(value.num_milliseconds() as u64)
88}
89
90/// Distribution metadata for distributed events. This contains metadata about
91/// the event deadline as well as the priority in which leaders should attempt
92/// to execute the event.
93#[derive(Clone, Debug, Serialize, Deserialize)]
94pub struct DistributedEventMetadata {
95    /// The execution window duration.
96    #[serde(
97        rename = "deadline_ms",
98        deserialize_with = "deserialize_u64_to_duration",
99        serialize_with = "serialize_duration_to_u64"
100    )]
101    pub deadline: chrono::Duration,
102    /// The timestamp by which the event was requested.
103    #[serde(
104        rename = "requested_at_ms",
105        deserialize_with = "deserialize_u64_to_datetime",
106        serialize_with = "serialize_datetime_to_u64"
107    )]
108    pub requested_at: chrono::DateTime<chrono::Utc>,
109    /// The priority list of leader addresses.
110    pub leaders: Vec<sui::types::Address>,
111    /// The task ID.
112    pub task_id: sui::types::Address,
113}
114
115/// Struct holding the Sui event ID, the event generic arguments and the data
116/// as one of [NexusEventKind].
117#[derive(Clone, Debug, Serialize, Deserialize)]
118pub struct NexusEvent {
119    /// The event transaction digest and event sequence.
120    pub id: (sui::types::Digest, u64),
121    /// Package containing the top level Move function that emitted the event.
122    ///
123    /// This identifies runtime code and is intentionally distinct from the
124    /// event datatype origin encoded in its type tag.
125    pub emitting_package: sui::types::Address,
126    /// If the `T in NexusEvent<T>` is also a generic, this field holds the
127    /// generic type. Note that this can be nested indefinitely.
128    pub generics: Vec<sui::types::TypeTag>,
129    /// The event data.
130    pub data: NexusEventKind,
131    /// If the event is a distributed event, this field holds the distribution
132    /// metadata.
133    pub distribution: Option<DistributedEventMetadata>,
134}
135
136impl NexusEvent {
137    /// Returns whether this event was emitted by a package in `context`.
138    ///
139    /// An operation pinned to a protocol version should use the configuration
140    /// captured at its boundary. This permits compatible in flight work to
141    /// finish without accepting an old package for newly created work.
142    pub fn was_emitted_by(&self, context: &crate::types::NexusContext) -> bool {
143        context
144            .packages()
145            .all()
146            .any(|package| package.storage_id == self.emitting_package)
147    }
148}
149
150/// Nexus event selected before source compatibility policy is applied.
151#[derive(Clone, Debug)]
152pub enum NexusEventCandidate {
153    /// Event represented by [`NexusEventKind`].
154    Supported(Box<NexusEvent>),
155    /// Event absent from [`NexusEventKind`].
156    Unsupported(UnsupportedNexusEvent),
157}
158
159impl NexusEventCandidate {
160    /// Returns the package recorded by Sui as the Move command source.
161    pub fn source_package(&self) -> sui::types::Address {
162        match self {
163            Self::Supported(event) => event.emitting_package,
164            Self::Unsupported(event) => event.source_package,
165        }
166    }
167
168    /// Converts [`Self::Supported`] into its decoded event.
169    ///
170    /// # Errors
171    ///
172    /// Returns [`UnsupportedNexusEvent`] for [`Self::Unsupported`].
173    pub fn into_supported(self) -> Result<NexusEvent, UnsupportedNexusEvent> {
174        match self {
175            Self::Supported(event) => Ok(*event),
176            Self::Unsupported(event) => Err(event),
177        }
178    }
179}
180
181macro_rules! events {
182    (
183        $(
184            $event_ty:ty => $variant:ident, $name:expr
185        ),* $(,)?
186    ) => {
187
188        // == enum NexusEventKind ==
189
190        #[derive(Clone, Debug, Serialize, Deserialize)]
191        #[serde(tag = "_nexus_event_type", content = "event")]
192        pub enum NexusEventKind {
193            $(
194                #[serde(rename = $name)]
195                $variant($event_ty),
196            )*
197        }
198
199        impl NexusEventKind {
200            /// Move datatype names supported by [`NexusEventKind`].
201            pub const NAMES: &'static [&'static str] = &[
202                $($name),*
203            ];
204
205            /// Returns the name of the event kind as a string.
206            pub fn name(&self) -> String {
207                match self {
208                    $(
209                        NexusEventKind::$variant(_) => $name.to_string(),
210                    )*
211                }
212            }
213        }
214
215        // == Parsing from BCS ==
216
217        pub(super) fn parse_bcs(name: &str, bytes: &[u8]) -> Result<(NexusEventKind, Option<DistributedEventMetadata>)> {
218            #[derive(Deserialize)]
219            struct Wrapper<T> {
220                event: T,
221            }
222
223            #[derive(Deserialize)]
224            struct DistributedWrapper<T> {
225                event: T,
226                deadline_ms: u64,
227                requested_at_ms: u64,
228                task_id: sui::types::Address,
229                leaders: Vec<sui::types::Address>,
230            }
231
232            match name {
233                $(
234                    $name => {
235                        match bcs::from_bytes::<DistributedWrapper<$event_ty>>(bytes) {
236                            Ok(distributed) => {
237                                let metadata = DistributedEventMetadata {
238                                    deadline: chrono::Duration::milliseconds(distributed.deadline_ms as i64),
239                                    requested_at: chrono::DateTime::<chrono::Utc>::from_timestamp_millis(distributed.requested_at_ms as i64)
240                                        .ok_or_else(|| anyhow::anyhow!("Invalid timestamp"))?,
241                                    task_id: distributed.task_id,
242                                    leaders: distributed.leaders,
243                                };
244
245                                Ok((NexusEventKind::$variant(distributed.event), Some(metadata)))
246                            }
247                            Err(_) => {
248                                 let obj: Wrapper<$event_ty> = bcs::from_bytes(bytes)?;
249
250                                 Ok((NexusEventKind::$variant(obj.event), None))
251                            }
252                        }
253                    }
254                )*
255                _ => bail!("Unknown event: {}", name),
256            }
257        }
258
259        pub(super) fn supports_event(name: &str) -> bool {
260            matches!(name, $($name)|*)
261        }
262
263        pub(super) fn event_struct_tag(
264            context: &crate::types::NexusContext,
265            name: &str,
266        ) -> Option<crate::sui::types::StructTag> {
267            match name {
268                $(
269                    $name => Some(crate::move_bindings::struct_tag::<$event_ty>(context)),
270                )*
271                _ => None,
272            }
273        }
274    };
275}
276
277// The SDK event contract is generated from the same committed IR that renders
278// the Move bindings above.
279include!(concat!(env!("OUT_DIR"), "/nexus_events.rs"));
280
281#[cfg(test)]
282mod tests {
283    use {super::NexusEventKind, std::collections::BTreeSet};
284
285    const PROTOCOL_IR: [&str; 6] = [
286        include_str!("../move_bindings/ir/interface.json"),
287        include_str!("../move_bindings/ir/primitives.json"),
288        include_str!("../move_bindings/ir/registry.json"),
289        include_str!("../move_bindings/ir/scheduler.json"),
290        include_str!("../move_bindings/ir/tool.json"),
291        include_str!("../move_bindings/ir/workflow.json"),
292    ];
293
294    #[test]
295    fn event_catalog_matches_committed_protocol_ir() {
296        let mut ir_names = PROTOCOL_IR
297            .into_iter()
298            .flat_map(|contents| {
299                let package: serde_json::Value =
300                    serde_json::from_str(contents).expect("committed IR is valid JSON");
301                package["modules"]
302                    .as_object()
303                    .expect("IR contains modules")
304                    .values()
305                    .flat_map(|module| {
306                        module["datatypes"]
307                            .as_array()
308                            .expect("IR module contains datatypes")
309                            .iter()
310                            .filter_map(|datatype| datatype["name"].as_str())
311                            .filter(|name| name.ends_with("Event"))
312                            .map(str::to_owned)
313                            .collect::<Vec<_>>()
314                    })
315                    .collect::<Vec<_>>()
316            })
317            .collect::<Vec<_>>();
318        let mut catalog_names = NexusEventKind::NAMES
319            .iter()
320            .map(|name| (*name).to_owned())
321            .collect::<Vec<_>>();
322
323        assert_eq!(
324            ir_names.len(),
325            ir_names.iter().collect::<BTreeSet<_>>().len(),
326            "Move event datatype names must be unique"
327        );
328        assert_eq!(
329            catalog_names.len(),
330            catalog_names.iter().collect::<BTreeSet<_>>().len(),
331            "SDK event names must be unique"
332        );
333
334        ir_names.sort();
335        catalog_names.sort();
336        assert_eq!(catalog_names, ir_names);
337    }
338}