use {
crate::sui,
anyhow::{bail, Result},
serde::{Deserialize, Serialize},
thiserror::Error,
};
mod parsing;
mod query;
pub use query::*;
#[derive(Clone, Debug, Error)]
#[error("Nexus event '{event_type}' from source package '{source_package}' is unsupported")]
pub struct UnsupportedNexusEvent {
pub id: (sui::types::Digest, u64),
pub source_package: sui::types::Address,
pub event_type: Box<sui::types::StructTag>,
}
#[derive(Debug, Error)]
pub enum NexusEventDecodeError {
#[error("Nexus event identity is invalid: {0}")]
Identity(String),
#[error("Required Nexus event field is missing: {0}")]
MissingField(&'static str),
#[error("Nexus event type is invalid: {0}")]
EventType(#[from] sui::types::TypeParseError),
#[error("Could not resolve Nexus event emitter package '{package}': {source}")]
EmitterPackage {
package: sui::types::Address,
#[source]
source: crate::nexus::error::NexusError,
},
#[error(transparent)]
UnsupportedEvent(#[from] UnsupportedNexusEvent),
#[error("Nexus event contents are invalid: {0}")]
Contents(#[source] anyhow::Error),
}
fn deserialize_u64_to_datetime<'de, D>(
deserializer: D,
) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
where
D: serde::Deserializer<'de>,
{
let timestamp = u64::deserialize(deserializer)?;
chrono::DateTime::from_timestamp_millis(timestamp as i64)
.ok_or_else(|| serde::de::Error::custom("datetime out of range"))
}
fn serialize_datetime_to_u64<S>(
value: &chrono::DateTime<chrono::Utc>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(value.timestamp_millis() as u64)
}
fn deserialize_u64_to_duration<'de, D>(deserializer: D) -> Result<chrono::Duration, D::Error>
where
D: serde::Deserializer<'de>,
{
let millis = u64::deserialize(deserializer)?;
Ok(chrono::Duration::milliseconds(millis as i64))
}
fn serialize_duration_to_u64<S>(value: &chrono::Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(value.num_milliseconds() as u64)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DistributedEventMetadata {
#[serde(
rename = "deadline_ms",
deserialize_with = "deserialize_u64_to_duration",
serialize_with = "serialize_duration_to_u64"
)]
pub deadline: chrono::Duration,
#[serde(
rename = "requested_at_ms",
deserialize_with = "deserialize_u64_to_datetime",
serialize_with = "serialize_datetime_to_u64"
)]
pub requested_at: chrono::DateTime<chrono::Utc>,
pub leaders: Vec<sui::types::Address>,
pub task_id: sui::types::Address,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NexusEvent {
pub id: (sui::types::Digest, u64),
pub emitting_package: sui::types::Address,
pub generics: Vec<sui::types::TypeTag>,
pub data: NexusEventKind,
pub distribution: Option<DistributedEventMetadata>,
}
impl NexusEvent {
pub fn was_emitted_by(&self, context: &crate::types::NexusContext) -> bool {
context
.packages()
.all()
.any(|package| package.storage_id == self.emitting_package)
}
}
#[derive(Clone, Debug)]
pub enum NexusEventCandidate {
Supported(Box<NexusEvent>),
Unsupported(UnsupportedNexusEvent),
}
impl NexusEventCandidate {
pub fn source_package(&self) -> sui::types::Address {
match self {
Self::Supported(event) => event.emitting_package,
Self::Unsupported(event) => event.source_package,
}
}
pub fn into_supported(self) -> Result<NexusEvent, UnsupportedNexusEvent> {
match self {
Self::Supported(event) => Ok(*event),
Self::Unsupported(event) => Err(event),
}
}
}
macro_rules! events {
(
$(
$event_ty:ty => $variant:ident, $name:expr
),* $(,)?
) => {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "_nexus_event_type", content = "event")]
pub enum NexusEventKind {
$(
#[serde(rename = $name)]
$variant($event_ty),
)*
}
impl NexusEventKind {
pub const NAMES: &'static [&'static str] = &[
$($name),*
];
pub fn name(&self) -> String {
match self {
$(
NexusEventKind::$variant(_) => $name.to_string(),
)*
}
}
}
pub(super) fn parse_bcs(name: &str, bytes: &[u8]) -> Result<(NexusEventKind, Option<DistributedEventMetadata>)> {
#[derive(Deserialize)]
struct Wrapper<T> {
event: T,
}
#[derive(Deserialize)]
struct DistributedWrapper<T> {
event: T,
deadline_ms: u64,
requested_at_ms: u64,
task_id: sui::types::Address,
leaders: Vec<sui::types::Address>,
}
match name {
$(
$name => {
match bcs::from_bytes::<DistributedWrapper<$event_ty>>(bytes) {
Ok(distributed) => {
let metadata = DistributedEventMetadata {
deadline: chrono::Duration::milliseconds(distributed.deadline_ms as i64),
requested_at: chrono::DateTime::<chrono::Utc>::from_timestamp_millis(distributed.requested_at_ms as i64)
.ok_or_else(|| anyhow::anyhow!("Invalid timestamp"))?,
task_id: distributed.task_id,
leaders: distributed.leaders,
};
Ok((NexusEventKind::$variant(distributed.event), Some(metadata)))
}
Err(_) => {
let obj: Wrapper<$event_ty> = bcs::from_bytes(bytes)?;
Ok((NexusEventKind::$variant(obj.event), None))
}
}
}
)*
_ => bail!("Unknown event: {}", name),
}
}
pub(super) fn supports_event(name: &str) -> bool {
matches!(name, $($name)|*)
}
pub(super) fn event_struct_tag(
context: &crate::types::NexusContext,
name: &str,
) -> Option<crate::sui::types::StructTag> {
match name {
$(
$name => Some(crate::move_bindings::struct_tag::<$event_ty>(context)),
)*
_ => None,
}
}
};
}
include!(concat!(env!("OUT_DIR"), "/nexus_events.rs"));
#[cfg(test)]
mod tests {
use {super::NexusEventKind, std::collections::BTreeSet};
const PROTOCOL_IR: [&str; 6] = [
include_str!("../move_bindings/ir/interface.json"),
include_str!("../move_bindings/ir/primitives.json"),
include_str!("../move_bindings/ir/registry.json"),
include_str!("../move_bindings/ir/scheduler.json"),
include_str!("../move_bindings/ir/tool.json"),
include_str!("../move_bindings/ir/workflow.json"),
];
#[test]
fn event_catalog_matches_committed_protocol_ir() {
let mut ir_names = PROTOCOL_IR
.into_iter()
.flat_map(|contents| {
let package: serde_json::Value =
serde_json::from_str(contents).expect("committed IR is valid JSON");
package["modules"]
.as_object()
.expect("IR contains modules")
.values()
.flat_map(|module| {
module["datatypes"]
.as_array()
.expect("IR module contains datatypes")
.iter()
.filter_map(|datatype| datatype["name"].as_str())
.filter(|name| name.ends_with("Event"))
.map(str::to_owned)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let mut catalog_names = NexusEventKind::NAMES
.iter()
.map(|name| (*name).to_owned())
.collect::<Vec<_>>();
assert_eq!(
ir_names.len(),
ir_names.iter().collect::<BTreeSet<_>>().len(),
"Move event datatype names must be unique"
);
assert_eq!(
catalog_names.len(),
catalog_names.iter().collect::<BTreeSet<_>>().len(),
"SDK event names must be unique"
);
ir_names.sort();
catalog_names.sort();
assert_eq!(catalog_names, ir_names);
}
}