1use {
4 super::{
5 event_struct_tag,
6 parsing::classify_nexus_event,
7 supports_event,
8 NexusEventCandidate,
9 NexusEventDecodeError,
10 },
11 crate::{
12 events::NexusEvent,
13 move_bindings::{
14 interface::distributed_event as distributed_event_move,
15 primitives::{data::NexusData as MoveNexusData, event as event_move},
16 },
17 nexus::state::StateResolver,
18 sui::{
19 self,
20 events::{EventIngestor, EventQuery},
21 },
22 types::{NexusContext, NexusObjects},
23 },
24 std::sync::Arc,
25 sui_rpc::{field::FieldMaskUtil as _, proto::sui::rpc::v2::filter::event as event_filter},
26};
27
28const DIRECT_FILTER_EXCLUSION_CANDIDATES: [&str; 8] = [
34 "ExecutionPaymentFeesRecordedEvent",
35 "GasPaymentConsumedEvent",
36 "InvocationLockedEvent",
37 "InvocationSettledEvent",
38 "PriorityFeeDepositCreatedEvent",
39 "ToolVerificationResolvedEvent",
40 "ExecutionPaymentToolCostSnapshottedEvent",
41 "TaskCreatedEvent",
42];
43
44#[derive(Clone)]
49pub struct NexusEventDecoder {
50 resolver: StateResolver,
51 objects: Arc<NexusObjects>,
52}
53
54impl NexusEventDecoder {
55 pub fn new(resolver: StateResolver, objects: Arc<NexusObjects>) -> Self {
57 Self { resolver, objects }
58 }
59
60 pub async fn decode_sui_event(
70 &self,
71 index: u64,
72 digest: sui::types::Digest,
73 event: &sui::types::Event,
74 ) -> Result<Option<NexusEvent>, NexusEventDecodeError> {
75 self.decode_parts(
76 index,
77 digest,
78 event.package_id,
79 &event.type_,
80 &event.contents,
81 )
82 .await
83 }
84
85 async fn decode_parts(
86 &self,
87 index: u64,
88 digest: sui::types::Digest,
89 emitting_package: sui::types::Address,
90 wrapper_type: &sui::types::StructTag,
91 contents: &[u8],
92 ) -> Result<Option<NexusEvent>, NexusEventDecodeError> {
93 if !crate::move_bindings::struct_shape_matches::<event_move::EventWrapper<MoveNexusData>>(
94 wrapper_type,
95 ) && !crate::move_bindings::struct_shape_matches::<
96 distributed_event_move::DistributedEventWrapper<MoveNexusData>,
97 >(wrapper_type)
98 {
99 return Ok(None);
100 }
101 let context = self
102 .resolver
103 .resolve_emitter_context(Arc::clone(&self.objects), emitting_package)
104 .await
105 .map_err(|source| NexusEventDecodeError::EmitterPackage {
106 package: emitting_package,
107 source,
108 })?;
109 match classify_parts(
110 &context,
111 index,
112 digest,
113 emitting_package,
114 wrapper_type,
115 contents,
116 )? {
117 Some(candidate) => candidate
118 .into_supported()
119 .map(Some)
120 .map_err(NexusEventDecodeError::from),
121 None => Ok(None),
122 }
123 }
124}
125
126#[derive(Clone)]
131pub struct NexusEventQuery {
132 filter_context: Arc<NexusContext>,
133 decoder: NexusEventDecoder,
134 supported_event_filter: Option<fn(&str) -> bool>,
135}
136
137impl NexusEventQuery {
138 pub fn new(filter_context: Arc<NexusContext>, decoder: NexusEventDecoder) -> Self {
140 Self {
141 filter_context,
142 decoder,
143 supported_event_filter: None,
144 }
145 }
146
147 pub fn with_supported_event_filter(mut self, filter: fn(&str) -> bool) -> Self {
153 self.supported_event_filter = Some(filter);
154 self
155 }
156
157 pub async fn decode_sui_event(
166 &self,
167 index: u64,
168 digest: sui::types::Digest,
169 event: &sui::types::Event,
170 ) -> Result<Option<NexusEvent>, NexusEventDecodeError> {
171 self.decoder.decode_sui_event(index, digest, event).await
172 }
173}
174
175fn classify_parts(
176 context: &NexusContext,
177 index: u64,
178 digest: sui::types::Digest,
179 source_package: sui::types::Address,
180 wrapper_type: &sui::types::StructTag,
181 contents: &[u8],
182) -> Result<Option<NexusEventCandidate>, NexusEventDecodeError> {
183 classify_nexus_event(
184 index,
185 digest,
186 source_package,
187 contents,
188 wrapper_type,
189 context,
190 )
191}
192
193impl EventQuery for NexusEventQuery {
194 type Error = NexusEventDecodeError;
195 type Output = NexusEventCandidate;
196
197 fn filter(&self) -> sui::grpc::EventFilter {
198 nexus_server_filter(&self.filter_context, self.supported_event_filter)
199 }
200
201 fn read_mask(&self) -> sui::grpc::FieldMask {
202 sui::grpc::FieldMask::from_paths(["package_id", "event_type", "contents"])
203 }
204
205 async fn decode(&self, event: sui::grpc::Event) -> Result<Option<Self::Output>, Self::Error> {
206 let wrapper_type = event
207 .event_type_opt()
208 .ok_or(NexusEventDecodeError::MissingField("event_type"))?
209 .parse()?;
210 if self
211 .supported_event_filter
212 .is_some_and(|filter| !should_decode_event(&wrapper_type, filter))
213 {
214 return Ok(None);
215 }
216 let digest = event
217 .transaction_digest
218 .as_deref()
219 .ok_or_else(|| {
220 NexusEventDecodeError::Identity("transaction digest is missing".to_owned())
221 })?
222 .parse()
223 .map_err(|error| {
224 NexusEventDecodeError::Identity(format!("transaction digest is invalid: {error}"))
225 })?;
226 let event_index = event
227 .event_index
228 .ok_or_else(|| NexusEventDecodeError::Identity("event index is missing".to_owned()))?;
229 let contents = event
230 .contents_opt()
231 .and_then(|contents| contents.value_opt())
232 .ok_or(NexusEventDecodeError::MissingField("contents"))?;
233 let emitting_package = event
234 .package_id
235 .as_deref()
236 .ok_or(NexusEventDecodeError::MissingField("package_id"))?
237 .parse()
238 .map_err(|error| {
239 NexusEventDecodeError::Identity(format!("emitting package is invalid: {error}"))
240 })?;
241
242 let context = self
243 .decoder
244 .resolver
245 .resolve_emitter_context(Arc::clone(&self.decoder.objects), emitting_package)
246 .await
247 .map_err(|source| NexusEventDecodeError::EmitterPackage {
248 package: emitting_package,
249 source,
250 })?;
251 classify_parts(
252 &context,
253 event_index.into(),
254 digest,
255 emitting_package,
256 &wrapper_type,
257 contents,
258 )
259 }
260}
261
262fn nexus_server_filter(
263 context: &NexusContext,
264 supported_event_filter: Option<fn(&str) -> bool>,
265) -> sui::grpc::EventFilter {
266 let wrapper =
267 crate::move_bindings::struct_tag::<event_move::EventWrapper<MoveNexusData>>(context);
268 let distributed_wrapper = crate::move_bindings::struct_tag::<
269 distributed_event_move::DistributedEventWrapper<MoveNexusData>,
270 >(context);
271
272 let mut direct = vec![event_filter::event_type(wrapper_head(&wrapper))];
273 if let Some(filter) = supported_event_filter {
274 direct.extend(
275 DIRECT_FILTER_EXCLUSION_CANDIDATES
276 .into_iter()
277 .filter(|name| !filter(name))
278 .filter_map(|name| event_struct_tag(context, name))
279 .map(|event| {
280 event_filter::event_type(instantiate_wrapper(&wrapper, event)).negate()
281 }),
282 );
283 }
284
285 sui::grpc::EventFilter::any([
286 sui::grpc::EventTerm::all(direct),
287 sui::grpc::EventTerm::all([event_filter::event_type(wrapper_head(&distributed_wrapper))]),
288 ])
289}
290
291fn wrapper_head(wrapper: &sui::types::StructTag) -> String {
292 format!(
293 "{}::{}::{}",
294 wrapper.address(),
295 wrapper.module(),
296 wrapper.name()
297 )
298}
299
300fn instantiate_wrapper(
301 wrapper: &sui::types::StructTag,
302 event: sui::types::StructTag,
303) -> sui::types::StructTag {
304 sui::types::StructTag::new(
305 *wrapper.address(),
306 wrapper.module().clone(),
307 wrapper.name().clone(),
308 vec![sui::types::TypeTag::Struct(Box::new(event))],
309 )
310}
311
312fn wrapped_event_name(wrapper: &sui::types::StructTag) -> Option<&str> {
313 let sui::types::TypeTag::Struct(event) = wrapper.type_params().first()? else {
314 return None;
315 };
316 Some(event.name().as_str())
317}
318
319fn should_decode_event(wrapper: &sui::types::StructTag, filter: fn(&str) -> bool) -> bool {
320 wrapped_event_name(wrapper).is_none_or(|name| !supports_event(name) || filter(name))
321}
322
323pub type NexusEventIngestor = EventIngestor<NexusEventQuery>;
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn event_wrapper(name: &str) -> sui::types::StructTag {
331 sui::types::StructTag::new(
332 sui::types::Address::from_static("0x1"),
333 "event".parse().unwrap(),
334 "EventWrapper".parse().unwrap(),
335 vec![sui::types::TypeTag::Struct(Box::new(
336 sui::types::StructTag::new(
337 sui::types::Address::from_static("0x2"),
338 "events".parse().unwrap(),
339 name.parse().unwrap(),
340 Vec::new(),
341 ),
342 ))],
343 )
344 }
345
346 #[test]
347 fn extracts_wrapped_event_name() {
348 assert_eq!(
349 wrapped_event_name(&event_wrapper("TaskCreatedEvent")),
350 Some("TaskCreatedEvent")
351 );
352 }
353
354 #[test]
355 fn filter_omits_known_events_but_retains_unknown_events() {
356 let only_tasks = |name: &str| name == "TaskCreatedEvent";
357
358 assert!(should_decode_event(
359 &event_wrapper("TaskCreatedEvent"),
360 only_tasks
361 ));
362 assert!(!should_decode_event(
363 &event_wrapper("DAGCreatedEvent"),
364 only_tasks
365 ));
366 assert!(should_decode_event(
367 &event_wrapper("FutureProtocolEvent"),
368 only_tasks
369 ));
370 }
371
372 #[test]
373 fn direct_filter_exclusions_match_the_generated_event_catalog() {
374 for (index, name) in DIRECT_FILTER_EXCLUSION_CANDIDATES.iter().enumerate() {
375 assert!(
376 crate::events::NexusEventKind::NAMES.contains(name),
377 "{name} is absent from the generated event catalog"
378 );
379 assert!(
380 !DIRECT_FILTER_EXCLUSION_CANDIDATES[..index].contains(name),
381 "{name} appears more than once"
382 );
383 }
384 }
385
386 #[cfg(feature = "test_utils")]
387 #[test]
388 fn server_filter_uses_the_portable_literal_budget() {
389 let context = crate::test_utils::sui_mocks::mock_nexus_context();
390 let filter = nexus_server_filter(&context, Some(|_| false));
391
392 assert_eq!(filter.terms.len(), 2);
393 assert_eq!(
394 filter
395 .terms
396 .iter()
397 .map(|term| term.literals.len())
398 .sum::<usize>(),
399 10
400 );
401 assert!(!filter.terms[0].literals[0].negated);
402 assert!(filter.terms[0].literals[1..]
403 .iter()
404 .all(|literal| literal.negated));
405 for literal in &filter.terms[0].literals[1..] {
406 let Some(sui::grpc::event_literal::Predicate::EventType(event_type)) =
407 &literal.predicate
408 else {
409 panic!("exclusion is not an event type")
410 };
411 let wrapper: sui::types::StructTag = event_type
412 .event_type
413 .as_deref()
414 .expect("event type is present")
415 .parse()
416 .expect("event type is valid");
417 let Some(sui::types::TypeTag::Struct(event)) = wrapper.type_params().first() else {
418 panic!("excluded wrapper has no inner event")
419 };
420 assert_ne!(*event.address(), sui::types::Address::ZERO);
421 }
422 assert_eq!(filter.terms[1].literals.len(), 1);
423 assert!(!filter.terms[1].literals[0].negated);
424 }
425
426 #[cfg(feature = "test_utils")]
427 #[test]
428 fn unfiltered_query_retains_both_wrapper_heads_without_exclusions() {
429 let context = crate::test_utils::sui_mocks::mock_nexus_context();
430 let filter = nexus_server_filter(&context, None);
431
432 assert_eq!(filter.terms.len(), 2);
433 assert!(filter
434 .terms
435 .iter()
436 .all(|term| term.literals.len() == 1 && !term.literals[0].negated));
437 }
438}