use {
crate::{sui, types::NexusObjects},
serde::{Deserialize, Serialize},
std::{collections::BTreeMap, ops::Deref, sync::Arc},
};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PackageRole {
Primitives,
Interface,
Tool,
Registry,
Workflow,
Scheduler,
}
impl PackageRole {
pub const fn as_str(self) -> &'static str {
match self {
Self::Primitives => "primitives",
Self::Interface => "interface",
Self::Tool => "tool",
Self::Registry => "registry",
Self::Workflow => "workflow",
Self::Scheduler => "scheduler",
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct DatatypeKey {
pub module: String,
pub datatype: String,
}
impl DatatypeKey {
pub fn new(module: impl Into<String>, datatype: impl Into<String>) -> Self {
Self {
module: module.into(),
datatype: datatype.into(),
}
}
}
pub type TypeOrigins = BTreeMap<String, BTreeMap<String, sui::types::Address>>;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PackageLink {
pub storage_id: sui::types::Address,
pub version: u64,
}
pub type PackageLinkage = BTreeMap<sui::types::Address, PackageLink>;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PackageVersion {
pub initial_id: sui::types::Address,
pub storage_id: sui::types::Address,
pub version: u64,
pub type_origins: TypeOrigins,
pub linkage: PackageLinkage,
}
impl PackageVersion {
pub fn new(
initial_id: sui::types::Address,
storage_id: sui::types::Address,
version: u64,
type_origins: TypeOrigins,
linkage: PackageLinkage,
) -> Self {
Self {
initial_id,
storage_id,
version,
type_origins,
linkage,
}
}
pub fn type_origin(&self, module: &str, datatype: &str) -> anyhow::Result<sui::types::Address> {
self.type_origins
.get(module)
.and_then(|types| types.get(datatype))
.copied()
.ok_or_else(|| {
anyhow::anyhow!(
"Package '{}' has no type origin for '{module}::{datatype}'",
self.storage_id
)
})
}
pub fn insert_type_origin(
&mut self,
key: DatatypeKey,
package_id: sui::types::Address,
) -> anyhow::Result<()> {
let previous = self
.type_origins
.entry(key.module.clone())
.or_default()
.insert(key.datatype.clone(), package_id);
if previous.is_some_and(|previous| previous != package_id) {
anyhow::bail!(
"Datatype '{}::{}' has conflicting package origins",
key.module,
key.datatype
);
}
Ok(())
}
pub fn contains_package(&self, address: sui::types::Address) -> bool {
address == self.initial_id
|| address == self.storage_id
|| self
.type_origins
.values()
.any(|types| types.values().any(|origin| *origin == address))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct NexusPackages {
pub primitives: Option<PackageVersion>,
pub interface: Option<PackageVersion>,
pub tool: Option<PackageVersion>,
pub registry: Option<PackageVersion>,
pub workflow: Option<PackageVersion>,
pub scheduler: Option<PackageVersion>,
}
impl NexusPackages {
pub const fn get(&self, role: PackageRole) -> Option<&PackageVersion> {
match role {
PackageRole::Primitives => self.primitives.as_ref(),
PackageRole::Interface => self.interface.as_ref(),
PackageRole::Tool => self.tool.as_ref(),
PackageRole::Registry => self.registry.as_ref(),
PackageRole::Workflow => self.workflow.as_ref(),
PackageRole::Scheduler => self.scheduler.as_ref(),
}
}
pub fn insert(&mut self, role: PackageRole, package: PackageVersion) -> Option<PackageVersion> {
match role {
PackageRole::Primitives => self.primitives.replace(package),
PackageRole::Interface => self.interface.replace(package),
PackageRole::Tool => self.tool.replace(package),
PackageRole::Registry => self.registry.replace(package),
PackageRole::Workflow => self.workflow.replace(package),
PackageRole::Scheduler => self.scheduler.replace(package),
}
}
pub fn all(&self) -> impl Iterator<Item = &PackageVersion> {
[
self.primitives.as_ref(),
self.interface.as_ref(),
self.tool.as_ref(),
self.registry.as_ref(),
self.workflow.as_ref(),
self.scheduler.as_ref(),
]
.into_iter()
.flatten()
}
pub fn contains_package(&self, address: sui::types::Address) -> bool {
self.all().any(|package| package.contains_package(address))
}
}
#[derive(Clone, Debug)]
pub struct NexusContext {
objects: Arc<NexusObjects>,
packages: NexusPackages,
}
impl NexusContext {
pub fn new(objects: Arc<NexusObjects>, packages: NexusPackages) -> Self {
Self { objects, packages }
}
pub fn objects(&self) -> &NexusObjects {
&self.objects
}
pub const fn packages(&self) -> &NexusPackages {
&self.packages
}
pub fn require_package(&self, role: PackageRole) -> anyhow::Result<&PackageVersion> {
self.packages.get(role).ok_or_else(|| {
anyhow::anyhow!(
"Operation package graph does not contain the '{}' role",
role.as_str()
)
})
}
pub fn package_id(&self, role: PackageRole) -> anyhow::Result<sui::types::Address> {
Ok(self.require_package(role)?.storage_id)
}
pub fn type_origin(
&self,
role: PackageRole,
module: &str,
datatype: &str,
) -> anyhow::Result<sui::types::Address> {
self.require_package(role)?.type_origin(module, datatype)
}
}
impl Deref for NexusContext {
type Target = NexusObjects;
fn deref(&self) -> &Self::Target {
self.objects()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn absent_roles_remain_absent() {
let mut graph = NexusPackages::default();
graph.insert(
PackageRole::Registry,
PackageVersion::new(
address("0xa1"),
address("0xa2"),
2,
TypeOrigins::new(),
PackageLinkage::new(),
),
);
assert!(graph.registry.is_some());
assert!(graph.workflow.is_none());
assert_eq!(graph.all().count(), 1);
}
#[test]
fn missing_type_origin_is_not_guessed() {
let package = PackageVersion::new(
address("0xa1"),
address("0xa2"),
2,
TypeOrigins::new(),
PackageLinkage::new(),
);
assert!(package.type_origin("era", "V1").is_err());
}
#[cfg(feature = "test_utils")]
#[test]
fn context_resolves_package_and_datatype_identity() {
let objects = Arc::new(crate::test_utils::sui_mocks::mock_nexus_objects());
let packages = crate::test_utils::sui_mocks::mock_nexus_packages();
let expected = packages.registry.as_ref().unwrap();
let expected_package = expected.storage_id;
let expected_origin = expected.type_origin("leader", "LeaderRegistry").unwrap();
let context = NexusContext::new(objects, packages);
assert_eq!(
context.package_id(PackageRole::Registry).unwrap(),
expected_package
);
assert_eq!(
context
.type_origin(PackageRole::Registry, "leader", "LeaderRegistry")
.unwrap(),
expected_origin
);
}
fn address(value: &'static str) -> sui::types::Address {
sui::types::Address::from_static(value)
}
}