Skip to main content

nexus_sdk/types/
package.rs

1//! Immutable package metadata for one Nexus operation.
2
3use {
4    crate::{sui, types::NexusObjects},
5    serde::{Deserialize, Serialize},
6    std::{collections::BTreeMap, ops::Deref, sync::Arc},
7};
8
9/// Canonical role of one Nexus Move package family.
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum PackageRole {
13    /// Common Nexus primitives.
14    Primitives,
15    /// Shared Nexus interfaces.
16    Interface,
17    /// Tool definitions, lifecycle, and payments.
18    Tool,
19    /// Canonical Nexus registries.
20    Registry,
21    /// Workflow execution.
22    Workflow,
23    /// Scheduled execution.
24    Scheduler,
25}
26
27impl PackageRole {
28    /// Returns the stable metadata name for this role.
29    pub const fn as_str(self) -> &'static str {
30        match self {
31            Self::Primitives => "primitives",
32            Self::Interface => "interface",
33            Self::Tool => "tool",
34            Self::Registry => "registry",
35            Self::Workflow => "workflow",
36            Self::Scheduler => "scheduler",
37        }
38    }
39}
40
41/// Exact identity of one Move datatype within a package family.
42#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
43pub struct DatatypeKey {
44    /// Move module that declares the datatype.
45    pub module: String,
46    /// Move datatype name within [`Self::module`].
47    pub datatype: String,
48}
49
50impl DatatypeKey {
51    /// Creates one module and datatype lookup key.
52    pub fn new(module: impl Into<String>, datatype: impl Into<String>) -> Self {
53        Self {
54            module: module.into(),
55            datatype: datatype.into(),
56        }
57    }
58}
59
60/// Datatype origins grouped by module and datatype name.
61///
62/// This shape is shared with generated Move binding package scopes.
63pub type TypeOrigins = BTreeMap<String, BTreeMap<String, sui::types::Address>>;
64
65/// Exact immutable target selected for one linked package lineage.
66#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
67pub struct PackageLink {
68    /// Immutable package object selected by the dependency.
69    pub storage_id: sui::types::Address,
70    /// Sui package version of [`Self::storage_id`].
71    pub version: u64,
72}
73
74/// Immutable linkage table keyed by initial package lineage ID.
75pub type PackageLinkage = BTreeMap<sui::types::Address, PackageLink>;
76
77/// Exact immutable metadata for one package version.
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct PackageVersion {
80    /// Stable ID of the first package in this upgrade lineage.
81    pub initial_id: sui::types::Address,
82    /// Immutable package object containing the selected code.
83    pub storage_id: sui::types::Address,
84    /// Sui package version declared by [`Self::storage_id`].
85    pub version: u64,
86    /// Exact package version that first defined each datatype.
87    pub type_origins: TypeOrigins,
88    /// Exact dependency versions recorded by the package object.
89    pub linkage: PackageLinkage,
90}
91
92impl PackageVersion {
93    /// Creates complete immutable package metadata.
94    pub fn new(
95        initial_id: sui::types::Address,
96        storage_id: sui::types::Address,
97        version: u64,
98        type_origins: TypeOrigins,
99        linkage: PackageLinkage,
100    ) -> Self {
101        Self {
102            initial_id,
103            storage_id,
104            version,
105            type_origins,
106            linkage,
107        }
108    }
109
110    /// Resolves one datatype identity.
111    ///
112    /// A missing origin is invalid package metadata rather than permission to
113    /// guess the package lineage.
114    pub fn type_origin(&self, module: &str, datatype: &str) -> anyhow::Result<sui::types::Address> {
115        self.type_origins
116            .get(module)
117            .and_then(|types| types.get(datatype))
118            .copied()
119            .ok_or_else(|| {
120                anyhow::anyhow!(
121                    "Package '{}' has no type origin for '{module}::{datatype}'",
122                    self.storage_id
123                )
124            })
125    }
126
127    /// Inserts one exact datatype origin and rejects conflicting metadata.
128    pub fn insert_type_origin(
129        &mut self,
130        key: DatatypeKey,
131        package_id: sui::types::Address,
132    ) -> anyhow::Result<()> {
133        let previous = self
134            .type_origins
135            .entry(key.module.clone())
136            .or_default()
137            .insert(key.datatype.clone(), package_id);
138        if previous.is_some_and(|previous| previous != package_id) {
139            anyhow::bail!(
140                "Datatype '{}::{}' has conflicting package origins",
141                key.module,
142                key.datatype
143            );
144        }
145        Ok(())
146    }
147
148    /// Returns whether `address` occurs in this package family metadata.
149    pub fn contains_package(&self, address: sui::types::Address) -> bool {
150        address == self.initial_id
151            || address == self.storage_id
152            || self
153                .type_origins
154                .values()
155                .any(|types| types.values().any(|origin| *origin == address))
156    }
157}
158
159/// Package dependency graph selected for one operation.
160///
161/// A role is [`None`] when the operation does not require that package. This
162/// prevents a zero address from being mistaken for resolved package authority.
163#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
164pub struct NexusPackages {
165    /// Common primitives when required by the operation.
166    pub primitives: Option<PackageVersion>,
167    /// Shared interfaces when required by the operation.
168    pub interface: Option<PackageVersion>,
169    /// Tool package when required by the operation.
170    pub tool: Option<PackageVersion>,
171    /// Registry package when required by the operation.
172    pub registry: Option<PackageVersion>,
173    /// Workflow package when required by the operation.
174    pub workflow: Option<PackageVersion>,
175    /// Scheduler package when required by the operation.
176    pub scheduler: Option<PackageVersion>,
177}
178
179impl NexusPackages {
180    /// Returns the package metadata selected for `role`.
181    pub const fn get(&self, role: PackageRole) -> Option<&PackageVersion> {
182        match role {
183            PackageRole::Primitives => self.primitives.as_ref(),
184            PackageRole::Interface => self.interface.as_ref(),
185            PackageRole::Tool => self.tool.as_ref(),
186            PackageRole::Registry => self.registry.as_ref(),
187            PackageRole::Workflow => self.workflow.as_ref(),
188            PackageRole::Scheduler => self.scheduler.as_ref(),
189        }
190    }
191
192    /// Inserts `package` for `role` and returns previous metadata, if any.
193    pub fn insert(&mut self, role: PackageRole, package: PackageVersion) -> Option<PackageVersion> {
194        match role {
195            PackageRole::Primitives => self.primitives.replace(package),
196            PackageRole::Interface => self.interface.replace(package),
197            PackageRole::Tool => self.tool.replace(package),
198            PackageRole::Registry => self.registry.replace(package),
199            PackageRole::Workflow => self.workflow.replace(package),
200            PackageRole::Scheduler => self.scheduler.replace(package),
201        }
202    }
203
204    /// Iterates over every package present in canonical role order.
205    pub fn all(&self) -> impl Iterator<Item = &PackageVersion> {
206        [
207            self.primitives.as_ref(),
208            self.interface.as_ref(),
209            self.tool.as_ref(),
210            self.registry.as_ref(),
211            self.workflow.as_ref(),
212            self.scheduler.as_ref(),
213        ]
214        .into_iter()
215        .flatten()
216    }
217
218    /// Returns whether `address` occurs in any selected package family.
219    pub fn contains_package(&self, address: sui::types::Address) -> bool {
220        self.all().any(|package| package.contains_package(address))
221    }
222}
223
224/// Stable environment and exact package graph for one operation.
225///
226/// A [`NexusContext`] is deliberately short lived. Mutable object state is not
227/// cached in it, and its [`NexusPackages`] apply only to the operation that
228/// requested them.
229#[derive(Clone, Debug)]
230pub struct NexusContext {
231    objects: Arc<NexusObjects>,
232    packages: NexusPackages,
233}
234
235impl NexusContext {
236    /// Creates an operation context from stable environment identity and a
237    /// resolved package graph.
238    pub fn new(objects: Arc<NexusObjects>, packages: NexusPackages) -> Self {
239        Self { objects, packages }
240    }
241
242    /// Returns the stable environment identity.
243    pub fn objects(&self) -> &NexusObjects {
244        &self.objects
245    }
246
247    /// Returns the exact package graph for this operation.
248    pub const fn packages(&self) -> &NexusPackages {
249        &self.packages
250    }
251
252    /// Returns package metadata for `role`.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error when the operation graph does not contain `role`.
257    pub fn require_package(&self, role: PackageRole) -> anyhow::Result<&PackageVersion> {
258        self.packages.get(role).ok_or_else(|| {
259            anyhow::anyhow!(
260                "Operation package graph does not contain the '{}' role",
261                role.as_str()
262            )
263        })
264    }
265
266    /// Returns the immutable package selected for `role`.
267    ///
268    /// # Errors
269    ///
270    /// Returns the same error as [`Self::require_package`] when the operation
271    /// graph does not contain `role`.
272    pub fn package_id(&self, role: PackageRole) -> anyhow::Result<sui::types::Address> {
273        Ok(self.require_package(role)?.storage_id)
274    }
275
276    /// Returns the defining package for one datatype in `role`.
277    ///
278    /// # Errors
279    ///
280    /// Returns the errors reported by [`Self::require_package`] and
281    /// [`PackageVersion::type_origin`].
282    pub fn type_origin(
283        &self,
284        role: PackageRole,
285        module: &str,
286        datatype: &str,
287    ) -> anyhow::Result<sui::types::Address> {
288        self.require_package(role)?.type_origin(module, datatype)
289    }
290}
291
292impl Deref for NexusContext {
293    type Target = NexusObjects;
294
295    fn deref(&self) -> &Self::Target {
296        self.objects()
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn absent_roles_remain_absent() {
306        let mut graph = NexusPackages::default();
307        graph.insert(
308            PackageRole::Registry,
309            PackageVersion::new(
310                address("0xa1"),
311                address("0xa2"),
312                2,
313                TypeOrigins::new(),
314                PackageLinkage::new(),
315            ),
316        );
317
318        assert!(graph.registry.is_some());
319        assert!(graph.workflow.is_none());
320        assert_eq!(graph.all().count(), 1);
321    }
322
323    #[test]
324    fn missing_type_origin_is_not_guessed() {
325        let package = PackageVersion::new(
326            address("0xa1"),
327            address("0xa2"),
328            2,
329            TypeOrigins::new(),
330            PackageLinkage::new(),
331        );
332
333        assert!(package.type_origin("era", "V1").is_err());
334    }
335
336    #[cfg(feature = "test_utils")]
337    #[test]
338    fn context_resolves_package_and_datatype_identity() {
339        let objects = Arc::new(crate::test_utils::sui_mocks::mock_nexus_objects());
340        let packages = crate::test_utils::sui_mocks::mock_nexus_packages();
341        let expected = packages.registry.as_ref().unwrap();
342        let expected_package = expected.storage_id;
343        let expected_origin = expected.type_origin("leader", "LeaderRegistry").unwrap();
344        let context = NexusContext::new(objects, packages);
345
346        assert_eq!(
347            context.package_id(PackageRole::Registry).unwrap(),
348            expected_package
349        );
350        assert_eq!(
351            context
352                .type_origin(PackageRole::Registry, "leader", "LeaderRegistry")
353                .unwrap(),
354            expected_origin
355        );
356    }
357
358    fn address(value: &'static str) -> sui::types::Address {
359        sui::types::Address::from_static(value)
360    }
361}