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#[derive(Clone, Debug, Error)]
15#[error("Nexus event '{event_type}' from source package '{source_package}' is unsupported")]
16pub struct UnsupportedNexusEvent {
17 pub id: (sui::types::Digest, u64),
19 pub source_package: sui::types::Address,
21 pub event_type: Box<sui::types::StructTag>,
23}
24
25#[derive(Debug, Error)]
27pub enum NexusEventDecodeError {
28 #[error("Nexus event identity is invalid: {0}")]
30 Identity(String),
31 #[error("Required Nexus event field is missing: {0}")]
33 MissingField(&'static str),
34 #[error("Nexus event type is invalid: {0}")]
36 EventType(#[from] sui::types::TypeParseError),
37 #[error("Could not resolve Nexus event emitter package '{package}': {source}")]
39 EmitterPackage {
40 package: sui::types::Address,
42 #[source]
44 source: crate::nexus::error::NexusError,
45 },
46 #[error(transparent)]
48 UnsupportedEvent(#[from] UnsupportedNexusEvent),
49 #[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#[derive(Clone, Debug, Serialize, Deserialize)]
94pub struct DistributedEventMetadata {
95 #[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 #[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 pub leaders: Vec<sui::types::Address>,
111 pub task_id: sui::types::Address,
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize)]
118pub struct NexusEvent {
119 pub id: (sui::types::Digest, u64),
121 pub emitting_package: sui::types::Address,
126 pub generics: Vec<sui::types::TypeTag>,
129 pub data: NexusEventKind,
131 pub distribution: Option<DistributedEventMetadata>,
134}
135
136impl NexusEvent {
137 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#[derive(Clone, Debug)]
152pub enum NexusEventCandidate {
153 Supported(Box<NexusEvent>),
155 Unsupported(UnsupportedNexusEvent),
157}
158
159impl NexusEventCandidate {
160 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 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 #[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 pub const NAMES: &'static [&'static str] = &[
202 $($name),*
203 ];
204
205 pub fn name(&self) -> String {
207 match self {
208 $(
209 NexusEventKind::$variant(_) => $name.to_string(),
210 )*
211 }
212 }
213 }
214
215 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
277include!(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}