1use {
4 crate::{sui, types::NexusObjects},
5 serde::{Deserialize, Serialize},
6 std::{collections::BTreeMap, ops::Deref, sync::Arc},
7};
8
9#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum PackageRole {
13 Primitives,
15 Interface,
17 Tool,
19 Registry,
21 Workflow,
23 Scheduler,
25}
26
27impl PackageRole {
28 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
43pub struct DatatypeKey {
44 pub module: String,
46 pub datatype: String,
48}
49
50impl DatatypeKey {
51 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
60pub type TypeOrigins = BTreeMap<String, BTreeMap<String, sui::types::Address>>;
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
67pub struct PackageLink {
68 pub storage_id: sui::types::Address,
70 pub version: u64,
72}
73
74pub type PackageLinkage = BTreeMap<sui::types::Address, PackageLink>;
76
77#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct PackageVersion {
80 pub initial_id: sui::types::Address,
82 pub storage_id: sui::types::Address,
84 pub version: u64,
86 pub type_origins: TypeOrigins,
88 pub linkage: PackageLinkage,
90}
91
92impl PackageVersion {
93 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 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 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 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#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
164pub struct NexusPackages {
165 pub primitives: Option<PackageVersion>,
167 pub interface: Option<PackageVersion>,
169 pub tool: Option<PackageVersion>,
171 pub registry: Option<PackageVersion>,
173 pub workflow: Option<PackageVersion>,
175 pub scheduler: Option<PackageVersion>,
177}
178
179impl NexusPackages {
180 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 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 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 pub fn contains_package(&self, address: sui::types::Address) -> bool {
220 self.all().any(|package| package.contains_package(address))
221 }
222}
223
224#[derive(Clone, Debug)]
230pub struct NexusContext {
231 objects: Arc<NexusObjects>,
232 packages: NexusPackages,
233}
234
235impl NexusContext {
236 pub fn new(objects: Arc<NexusObjects>, packages: NexusPackages) -> Self {
239 Self { objects, packages }
240 }
241
242 pub fn objects(&self) -> &NexusObjects {
244 &self.objects
245 }
246
247 pub const fn packages(&self) -> &NexusPackages {
249 &self.packages
250 }
251
252 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 pub fn package_id(&self, role: PackageRole) -> anyhow::Result<sui::types::Address> {
273 Ok(self.require_package(role)?.storage_id)
274 }
275
276 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}