1#[cfg(feature = "signed_http")]
23use crate::signed_http::v3::wire::{
24 AllowedLeaderFileV1,
25 AllowedLeaderKeyFileV1,
26 AllowedLeadersFileV1,
27};
28use {
29 crate::{
30 move_bindings::{
31 registry::{
32 era::V1 as RegistryWitnessV1,
33 network_auth::{
34 IdentityKey,
35 KeyBinding,
36 KeyBindingInnerV1,
37 KeyRecord,
38 NetworkAuth,
39 NetworkAuthInnerV1,
40 },
41 },
42 tool::{
43 era::V1 as ToolWitnessV1,
44 tool_registry::{Tool as ToolAnchor, ToolInnerV1},
45 },
46 },
47 nexus::{
48 client::NexusClient,
49 crawler::{Crawler, Response},
50 error::NexusError,
51 state::StateResolver,
52 },
53 sui,
54 transactions::{self, tool::OffChainToolRegistration},
55 types::{NexusContext, PackageRole, Tool, ToolMeta},
56 ToolFqn,
57 },
58 ed25519_dalek::{Signature, Signer as _, SigningKey},
59 std::sync::Arc,
60};
61
62const POP_DOMAIN_V1: &[u8] = b"nexus_registry.network_auth.pop_v1";
63const KEY_SCHEME_ED25519: u8 = 0;
64
65#[derive(Clone, Debug)]
67pub struct RegisteredToolKey {
68 pub tx_digest: sui::types::Digest,
70 pub tool_id: sui::types::Address,
72 pub tool_kid: u64,
74 pub public_key: [u8; 32],
76 pub binding_object_id: sui::types::Address,
78}
79
80#[derive(Clone, Debug)]
82pub struct ToolKeyEntry {
83 pub kid: u64,
85 pub public_key_hex: String,
87 pub added_at_ms: u64,
89 pub revoked: bool,
91}
92
93#[derive(Clone, Debug)]
95pub struct ToolKeyList {
96 pub binding_object_id: sui::types::Address,
98 pub active_key_id: Option<u64>,
100 pub next_key_id: u64,
102 pub keys: Vec<ToolKeyEntry>,
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub struct ActiveEd25519Key {
109 pub kid: u64,
111 pub public_key: [u8; 32],
113}
114
115pub struct ResolvedKeyBinding {
117 pub binding: Response<KeyBindingInnerV1>,
119 pub active_key: Option<ActiveEd25519Key>,
121}
122
123pub struct NetworkAuthActions {
125 pub(super) client: NexusClient,
126}
127
128impl NetworkAuthActions {
129 pub async fn binding_object_id(
136 &self,
137 identity: &IdentityKey,
138 ) -> Result<sui::types::Address, NexusError> {
139 let context = self.network_auth_context().await?;
140 network_auth_codec(&context)?.binding_object_id(identity)
141 }
142
143 pub async fn register_tool_message_key(
155 &self,
156 tool_fqn: ToolFqn,
157 owner_cap_over_tool: sui::types::Address,
158 tool_signing_key: SigningKey,
159 description: Option<Vec<u8>>,
160 ) -> Result<RegisteredToolKey, NexusError> {
161 let address = self.client.owner()?;
162 let objects = self.client.get_nexus_objects();
163 let tool_id =
164 Tool::derive_id(objects.tool_registry.object_id(), &tool_fqn).map_err(|e| {
165 NexusError::Parsing(anyhow::anyhow!(
166 "failed to derive ToolId for FQN '{tool_fqn}': {e}"
167 ))
168 })?;
169 let identity = IdentityKey::tool(tool_id);
170 let root_context = self.network_auth_context().await?;
171 let binding_object_id = network_auth_codec(&root_context)?.binding_object_id(&identity)?;
172
173 let existing = Self::try_get_key_binding(&self.client, binding_object_id).await?;
174 let (context, binding_ref, next_key_id) = match existing {
175 None => (root_context, None, 0),
176 Some((context, binding)) => {
177 validate_binding_identity(binding_object_id, &binding.data, &identity)?;
178 let next_key_id = binding.data.next_key_id;
179 (context, Some(binding.object_ref()), next_key_id)
180 }
181 };
182
183 let registry_storage = context
184 .require_package(PackageRole::Registry)
185 .map_err(|error| NexusError::Configuration(error.to_string()))?
186 .storage_id;
187 context
188 .require_package(PackageRole::Tool)
189 .map_err(|error| NexusError::IncompatiblePackage {
190 package: registry_storage,
191 reason: error.to_string(),
192 })?;
193 self.client
194 .state_resolver()
195 .validate_state_pair::<ToolAnchor, ToolWitnessV1, ToolInnerV1>(tool_id, &context)
196 .await?;
197
198 let (public_key, pop_sig) = tool_key_material(&identity, next_key_id, &tool_signing_key)?;
199 let (owner_cap_ref, tool) = tokio::try_join!(
200 self.client.object_reference(owner_cap_over_tool),
201 self.client.object_reference(tool_id),
202 )?;
203
204 let tx = match binding_ref {
205 None => transactions::network_auth::create_tool_binding_and_register_key_ptb(
206 &context,
207 &tool,
208 &owner_cap_ref,
209 public_key,
210 pop_sig,
211 description,
212 ),
213 Some(binding_ref) => {
214 transactions::network_auth::register_tool_key_on_existing_binding_ptb(
215 &context,
216 &binding_ref,
217 &tool,
218 &owner_cap_ref,
219 public_key,
220 pop_sig,
221 )
222 }
223 }
224 .map_err(NexusError::TransactionBuilding)?;
225
226 let response = self.client.submit_transaction(tx, address).await?;
227
228 Ok(RegisteredToolKey {
229 tx_digest: response.digest,
230 tool_id,
231 tool_kid: next_key_id,
232 public_key,
233 binding_object_id,
234 })
235 }
236
237 pub async fn list_tool_keys(
246 &self,
247 tool_fqn: &ToolFqn,
248 ) -> Result<Option<ToolKeyList>, NexusError> {
249 let objects = self.client.get_nexus_objects();
250 let context = self.network_auth_context().await?;
251 let codec = network_auth_codec(&context)?;
252 let tool_id =
253 Tool::derive_id(objects.tool_registry.object_id(), tool_fqn).map_err(|e| {
254 NexusError::Parsing(anyhow::anyhow!(
255 "failed to derive ToolId for FQN '{tool_fqn}': {e}"
256 ))
257 })?;
258 let identity = IdentityKey::tool(tool_id);
259 let binding_object_id = codec.binding_object_id(&identity)?;
260
261 let (_, binding) = match Self::try_get_key_binding(&self.client, binding_object_id).await? {
262 None => return Ok(None),
263 Some(binding) => binding,
264 };
265 validate_binding_identity(binding_object_id, &binding.data, &identity)?;
266
267 let key_records = self
268 .client
269 .crawler()
270 .get_dynamic_fields::<u64, KeyRecord>(
271 binding.data.key_table_id(),
272 binding.data.key_table_size(),
273 )
274 .await
275 .map_err(|e| {
276 NexusError::Rpc(anyhow::anyhow!(
277 "failed to fetch tool key records ({binding_object_id}): {e}"
278 ))
279 })?;
280
281 let mut keys: Vec<ToolKeyEntry> = key_records
282 .into_iter()
283 .map(|(kid, record)| ToolKeyEntry {
284 kid,
285 public_key_hex: hex::encode(&record.public_key),
286 added_at_ms: record.added_at_ms,
287 revoked: record.revoked_at_ms().is_some(),
288 })
289 .collect();
290 keys.sort_by_key(|k| k.kid);
291
292 Ok(Some(ToolKeyList {
293 binding_object_id,
294 active_key_id: binding.data.active_key_id(),
295 next_key_id: binding.data.next_key_id,
296 keys,
297 }))
298 }
299
300 #[cfg(feature = "signed_http")]
307 pub async fn export_allowed_leaders_file_v1(
308 &self,
309 leader_cap_ids: &[sui::types::Address],
310 ) -> Result<AllowedLeadersFileV1, NexusError> {
311 let context = self.network_auth_context().await?;
312 let codec = network_auth_codec(&context)?;
313
314 let mut out = Vec::with_capacity(leader_cap_ids.len());
315 for leader_cap_id in leader_cap_ids {
316 let identity = IdentityKey::leader(*leader_cap_id);
317 let binding_object_id = codec.binding_object_id(&identity)?;
318 let (_, binding) = Self::try_get_key_binding(&self.client, binding_object_id)
319 .await?
320 .ok_or_else(|| {
321 NexusError::Rpc(anyhow::anyhow!(
322 "leader KeyBinding '{binding_object_id}' was not found"
323 ))
324 })?;
325 validate_binding_identity(binding_object_id, &binding.data, &identity)?;
326
327 let active_kid = binding.data.active_key_id().ok_or_else(|| {
328 NexusError::Parsing(anyhow::anyhow!(
329 "leader binding {binding_object_id} has no active key"
330 ))
331 })?;
332
333 let record = fetch_key_record(self.client.crawler(), &binding, active_kid).await?;
334 let public_key: [u8; 32] = record.public_key.as_slice().try_into().map_err(|_| {
335 NexusError::Parsing(anyhow::anyhow!(
336 "leader binding {binding_object_id} active key is not 32 bytes"
337 ))
338 })?;
339
340 if record.scheme != KEY_SCHEME_ED25519 {
341 return Err(NexusError::Parsing(anyhow::anyhow!(
342 "leader binding {binding_object_id} active key uses unsupported scheme {}",
343 record.scheme
344 )));
345 }
346
347 out.push(AllowedLeaderFileV1 {
348 leader_id: leader_cap_id.to_string(),
349 keys: vec![AllowedLeaderKeyFileV1 {
350 kid: active_kid,
351 public_key: hex::encode(public_key),
352 }],
353 });
354 }
355
356 Ok(AllowedLeadersFileV1 {
357 version: 1,
358 leaders: out,
359 })
360 }
361
362 pub async fn list_leader_cap_ids_from_network_auth(
369 &self,
370 ) -> Result<Vec<sui::types::Address>, NexusError> {
371 let (_, state) = self.network_auth_state().await?;
372 Ok(sorted_leader_cap_ids(&state.data))
373 }
374
375 #[cfg(feature = "signed_http")]
379 pub async fn export_allowed_leaders_file_v1_for_all_leaders(
380 &self,
381 ) -> Result<AllowedLeadersFileV1, NexusError> {
382 let (context, state) = self.network_auth_state().await?;
383 let leader_cap_ids = sorted_leader_cap_ids(&state.data);
384 if leader_cap_ids.is_empty() {
385 return Err(NexusError::Parsing(anyhow::anyhow!(
386 "network_auth contains no leader identities"
387 )));
388 }
389
390 let codec = network_auth_codec(&context)?;
391
392 let mut out = Vec::with_capacity(leader_cap_ids.len());
393 for leader_cap_id in leader_cap_ids {
394 if let Some(entry) =
395 Self::export_allowed_leader_entry_file_v1(&self.client, &codec, leader_cap_id)
396 .await?
397 {
398 out.push(entry);
399 }
400 }
401
402 if out.is_empty() {
403 return Err(NexusError::Parsing(anyhow::anyhow!(
404 "no leaders with an active Ed25519 key were found in network_auth"
405 )));
406 }
407
408 Ok(AllowedLeadersFileV1 {
409 version: 1,
410 leaders: out,
411 })
412 }
413
414 async fn network_auth_context(&self) -> Result<Arc<NexusContext>, NexusError> {
415 let objects = self.client.get_nexus_objects();
416 self.client.context_for_root(&objects.network_auth).await
417 }
418
419 async fn network_auth_state(
420 &self,
421 ) -> Result<(Arc<NexusContext>, Response<NetworkAuthInnerV1>), NexusError> {
422 let context = self.network_auth_context().await?;
423 let object_id = context.network_auth.object_id();
424 let state = self
425 .client
426 .state_resolver()
427 .load_inner::<NetworkAuth, RegistryWitnessV1, NetworkAuthInnerV1>(object_id, &context)
428 .await?;
429 Ok((context, state))
430 }
431
432 async fn try_get_key_binding(
433 client: &NexusClient,
434 binding_object_id: sui::types::Address,
435 ) -> Result<Option<(Arc<NexusContext>, Response<KeyBindingInnerV1>)>, NexusError> {
436 let Some(_) = client
437 .crawler()
438 .get_optional_object::<KeyBinding>(binding_object_id)
439 .await
440 .map_err(NexusError::Rpc)?
441 else {
442 return Ok(None);
443 };
444 let context = client.context_for_object(binding_object_id).await?;
445 let binding = client
446 .state_resolver()
447 .load_inner::<KeyBinding, RegistryWitnessV1, KeyBindingInnerV1>(
448 binding_object_id,
449 &context,
450 )
451 .await?;
452 Ok(Some((context, binding)))
453 }
454
455 #[cfg(feature = "signed_http")]
456 async fn export_allowed_leader_entry_file_v1(
457 client: &NexusClient,
458 codec: &NetworkAuthCodec,
459 leader_cap_id: sui::types::Address,
460 ) -> Result<Option<AllowedLeaderFileV1>, NexusError> {
461 let identity = IdentityKey::leader(leader_cap_id);
462 let binding_object_id = codec.binding_object_id(&identity)?;
463 let (_, binding) = Self::try_get_key_binding(client, binding_object_id)
464 .await?
465 .ok_or_else(|| {
466 NexusError::Rpc(anyhow::anyhow!(
467 "leader KeyBinding '{binding_object_id}' was not found"
468 ))
469 })?;
470 validate_binding_identity(binding_object_id, &binding.data, &identity)?;
471
472 let Some(active_kid) = binding.data.active_key_id() else {
473 return Ok(None);
474 };
475
476 let record = fetch_key_record(client.crawler(), &binding, active_kid).await?;
477 let public_key: [u8; 32] = record.public_key.as_slice().try_into().map_err(|_| {
478 NexusError::Parsing(anyhow::anyhow!(
479 "leader binding {binding_object_id} active key is not 32 bytes"
480 ))
481 })?;
482
483 if record.scheme != KEY_SCHEME_ED25519 {
484 return Err(NexusError::Parsing(anyhow::anyhow!(
485 "leader binding {binding_object_id} active key uses unsupported scheme {}",
486 record.scheme
487 )));
488 }
489
490 Ok(Some(AllowedLeaderFileV1 {
491 leader_id: leader_cap_id.to_string(),
492 keys: vec![AllowedLeaderKeyFileV1 {
493 kid: active_kid,
494 public_key: hex::encode(public_key),
495 }],
496 }))
497 }
498}
499
500#[derive(Clone)]
506pub struct NetworkAuthReader {
507 crawler: Crawler,
508 state_resolver: StateResolver,
509 registry_type_origin_pkg_id: sui::types::Address,
510 network_auth_object_id: sui::types::Address,
511}
512
513impl NetworkAuthReader {
514 pub fn new(
521 crawler: Crawler,
522 registry_type_origin_pkg_id: sui::types::Address,
523 network_auth_object_id: sui::types::Address,
524 ) -> Self {
525 let state_resolver = StateResolver::new(Arc::new(crawler.clone()));
526 Self {
527 crawler,
528 state_resolver,
529 registry_type_origin_pkg_id,
530 network_auth_object_id,
531 }
532 }
533
534 pub fn from_client(
542 client: &NexusClient,
543 registry_type_origin_pkg_id: sui::types::Address,
544 network_auth_object_id: sui::types::Address,
545 ) -> Self {
546 Self {
547 crawler: client.crawler().clone(),
548 state_resolver: client.state_resolver().clone(),
549 registry_type_origin_pkg_id,
550 network_auth_object_id,
551 }
552 }
553
554 pub fn from_rpc_url(
560 rpc_url: &str,
561 registry_type_origin_pkg_id: sui::types::Address,
562 network_auth_object_id: sui::types::Address,
563 ) -> Result<Self, NexusError> {
564 let client = sui::grpc::client(rpc_url).map_err(NexusError::Rpc)?;
565 let crawler = Crawler::new(Arc::new(client));
566 Ok(Self::new(
567 crawler,
568 registry_type_origin_pkg_id,
569 network_auth_object_id,
570 ))
571 }
572
573 pub fn binding_object_id(
579 &self,
580 identity: &IdentityKey,
581 ) -> Result<sui::types::Address, NexusError> {
582 NetworkAuthCodec::new(
583 self.registry_type_origin_pkg_id,
584 self.network_auth_object_id,
585 )
586 .binding_object_id(identity)
587 }
588
589 pub async fn try_get_key_binding(
596 &self,
597 identity: &IdentityKey,
598 ) -> Result<Option<Response<KeyBindingInnerV1>>, NexusError> {
599 let binding_object_id = self.binding_object_id(identity)?;
600 try_get_key_binding_by_object_id(&self.crawler, &self.state_resolver, binding_object_id)
601 .await
602 }
603
604 pub async fn try_get_active_key_binding(
611 &self,
612 identity: &IdentityKey,
613 ) -> Result<Option<ResolvedKeyBinding>, NexusError> {
614 let Some(binding) = self.try_get_key_binding(identity).await? else {
615 return Ok(None);
616 };
617 let active_key = try_get_active_ed25519_key(&self.crawler, &binding).await?;
618
619 Ok(Some(ResolvedKeyBinding {
620 binding,
621 active_key,
622 }))
623 }
624
625 pub async fn list_leader_cap_ids_from_network_auth(
632 &self,
633 ) -> Result<Vec<sui::types::Address>, NexusError> {
634 let state = self
635 .state_resolver
636 .resolve_and_load_inner::<NetworkAuth, RegistryWitnessV1, NetworkAuthInnerV1>(
637 self.network_auth_object_id,
638 )
639 .await?;
640 Ok(sorted_leader_cap_ids(&state.data))
641 }
642
643 #[cfg(feature = "signed_http")]
647 pub async fn export_allowed_leaders_file_v1_for_all_leaders(
648 &self,
649 ) -> Result<AllowedLeadersFileV1, NexusError> {
650 let leader_cap_ids = self.list_leader_cap_ids_from_network_auth().await?;
651 if leader_cap_ids.is_empty() {
652 return Err(NexusError::Parsing(anyhow::anyhow!(
653 "network_auth contains no leader identities"
654 )));
655 }
656
657 let codec = NetworkAuthCodec::new(
658 self.registry_type_origin_pkg_id,
659 self.network_auth_object_id,
660 );
661
662 let mut out = Vec::with_capacity(leader_cap_ids.len());
663 for leader_cap_id in leader_cap_ids {
664 if let Some(entry) = self
665 .export_allowed_leader_entry_file_v1(&codec, leader_cap_id)
666 .await?
667 {
668 out.push(entry);
669 }
670 }
671
672 if out.is_empty() {
673 return Err(NexusError::Parsing(anyhow::anyhow!(
674 "no leaders with an active Ed25519 key were found in network_auth"
675 )));
676 }
677
678 Ok(AllowedLeadersFileV1 {
679 version: 1,
680 leaders: out,
681 })
682 }
683
684 #[cfg(feature = "signed_http")]
685 async fn export_allowed_leader_entry_file_v1(
686 &self,
687 codec: &NetworkAuthCodec,
688 leader_cap_id: sui::types::Address,
689 ) -> Result<Option<AllowedLeaderFileV1>, NexusError> {
690 let identity = IdentityKey::leader(leader_cap_id);
691 let Some(binding) = self.try_get_active_key_binding(&identity).await? else {
692 let binding_object_id = codec.binding_object_id(&identity)?;
693 return Err(NexusError::Rpc(anyhow::anyhow!(
694 "failed to fetch leader KeyBinding ({binding_object_id}): not found"
695 )));
696 };
697
698 let Some(active_key) = binding.active_key else {
699 return Ok(None);
700 };
701
702 Ok(Some(AllowedLeaderFileV1 {
703 leader_id: leader_cap_id.to_string(),
704 keys: vec![AllowedLeaderKeyFileV1 {
705 kid: active_key.kid,
706 public_key: hex::encode(active_key.public_key),
707 }],
708 }))
709 }
710}
711
712async fn try_get_key_binding_by_object_id(
713 crawler: &Crawler,
714 state_resolver: &StateResolver,
715 binding_object_id: sui::types::Address,
716) -> Result<Option<Response<KeyBindingInnerV1>>, NexusError> {
717 let Some(_) = crawler
718 .get_optional_object::<KeyBinding>(binding_object_id)
719 .await
720 .map_err(NexusError::Rpc)?
721 else {
722 return Ok(None);
723 };
724
725 state_resolver
726 .resolve_and_load_inner::<KeyBinding, RegistryWitnessV1, KeyBindingInnerV1>(
727 binding_object_id,
728 )
729 .await
730 .map(Some)
731}
732
733fn sorted_leader_cap_ids(state: &NetworkAuthInnerV1) -> Vec<sui::types::Address> {
734 let mut leaders = state.leader_cap_ids().collect::<Vec<_>>();
735 leaders.sort_unstable();
736 leaders.dedup();
737 leaders
738}
739
740async fn try_get_active_ed25519_key(
741 crawler: &Crawler,
742 binding: &Response<KeyBindingInnerV1>,
743) -> Result<Option<ActiveEd25519Key>, NexusError> {
744 let Some(active_kid) = binding.data.active_key_id() else {
745 return Ok(None);
746 };
747
748 let record = fetch_key_record(crawler, binding, active_kid).await?;
749 let public_key: [u8; 32] = record.public_key.as_slice().try_into().map_err(|_| {
750 NexusError::Parsing(anyhow::anyhow!(
751 "key binding {} active key kid={active_kid} is not 32 bytes",
752 binding.object_id
753 ))
754 })?;
755
756 if record.scheme != KEY_SCHEME_ED25519 {
757 return Err(NexusError::Parsing(anyhow::anyhow!(
758 "key binding {} active key kid={active_kid} uses unsupported scheme {}",
759 binding.object_id,
760 record.scheme
761 )));
762 }
763
764 if record.revoked_at_ms().is_some() {
765 return Err(NexusError::Parsing(anyhow::anyhow!(
766 "key binding {} active key kid={active_kid} is revoked",
767 binding.object_id
768 )));
769 }
770
771 Ok(Some(ActiveEd25519Key {
772 kid: active_kid,
773 public_key,
774 }))
775}
776
777async fn fetch_key_record(
778 crawler: &Crawler,
779 binding: &Response<KeyBindingInnerV1>,
780 key_id: u64,
781) -> Result<KeyRecord, NexusError> {
782 crawler
783 .get_dynamic_field_by_key::<u64, KeyRecord>(
784 binding.data.key_table_id(),
785 key_id,
786 &sui::types::TypeTag::U64,
787 )
788 .await
789 .map_err(|error| {
790 NexusError::Rpc(anyhow::anyhow!(
791 "failed to fetch key record ({}, kid={key_id}): {error}",
792 binding.object_id
793 ))
794 })?
795 .ok_or_else(|| {
796 NexusError::Parsing(anyhow::anyhow!(
797 "key binding {} is missing key record kid={key_id}",
798 binding.object_id
799 ))
800 })
801}
802
803fn network_auth_codec(context: &NexusContext) -> Result<NetworkAuthCodec, NexusError> {
804 let registry = context
805 .require_package(PackageRole::Registry)
806 .map_err(|error| NexusError::Configuration(error.to_string()))?;
807 let identity_origin = registry
808 .type_origin("network_auth", "IdentityKey")
809 .map_err(|error| NexusError::IncompatiblePackage {
810 package: registry.storage_id,
811 reason: error.to_string(),
812 })?;
813
814 Ok(NetworkAuthCodec::new(
815 identity_origin,
816 context.network_auth.object_id(),
817 ))
818}
819
820fn validate_binding_identity(
821 binding_object_id: sui::types::Address,
822 binding: &KeyBindingInnerV1,
823 expected: &IdentityKey,
824) -> Result<(), NexusError> {
825 if &binding.identity == expected {
826 return Ok(());
827 }
828
829 Err(NexusError::InvalidObjectState {
830 object: binding_object_id,
831 reason: "stored binding identity does not match its derived object ID".to_owned(),
832 })
833}
834
835struct NetworkAuthCodec {
837 registry_type_origin_pkg_id: sui::types::Address,
838 network_auth_object_id: sui::types::Address,
839}
840
841impl NetworkAuthCodec {
842 fn new(
843 registry_type_origin_pkg_id: sui::types::Address,
844 network_auth_object_id: sui::types::Address,
845 ) -> Self {
846 Self {
847 registry_type_origin_pkg_id,
848 network_auth_object_id,
849 }
850 }
851
852 fn binding_object_id(&self, identity: &IdentityKey) -> Result<sui::types::Address, NexusError> {
853 crate::move_bindings::derive_network_auth_binding_id(
854 self.registry_type_origin_pkg_id,
855 self.network_auth_object_id,
856 identity,
857 )
858 .map_err(NexusError::Parsing)
859 }
860}
861
862pub fn initial_tool_registration(
869 tool_registry_id: sui::types::Address,
870 meta: ToolMeta,
871 signing_key: &SigningKey,
872 invocation_cost_mist: u64,
873) -> Result<OffChainToolRegistration, NexusError> {
874 let tool_id = Tool::derive_id(tool_registry_id, &meta.fqn).map_err(|error| {
875 NexusError::Parsing(anyhow::anyhow!(
876 "failed to derive Tool ID for FQN '{}': {error}",
877 meta.fqn
878 ))
879 })?;
880 let identity = IdentityKey::tool(tool_id);
881 let (public_key, pop_signature) = tool_key_material(&identity, 0, signing_key)?;
882
883 Ok(OffChainToolRegistration {
884 meta,
885 public_key,
886 pop_signature,
887 invocation_cost_mist,
888 })
889}
890
891fn tool_key_material(
892 identity: &IdentityKey,
893 key_id: u64,
894 signing_key: &SigningKey,
895) -> Result<([u8; 32], [u8; 64]), NexusError> {
896 let public_key = signing_key.verifying_key().to_bytes();
897 let message = pop_message_v1(identity, key_id, public_key)?;
898 Ok((public_key, sign_bytes(signing_key, &message)))
899}
900
901fn pop_message_v1(
902 identity: &IdentityKey,
903 key_id: u64,
904 public_key: [u8; 32],
905) -> Result<Vec<u8>, NexusError> {
906 let mut out = Vec::new();
907 out.extend_from_slice(POP_DOMAIN_V1);
908 out.extend_from_slice(&identity_bcs(identity)?);
909 out.extend_from_slice(&bcs::to_bytes(&key_id).map_err(|error| {
910 NexusError::Parsing(anyhow::anyhow!("failed to BCS encode key_id: {error}"))
911 })?);
912 out.extend_from_slice(&public_key);
913 Ok(out)
914}
915
916fn identity_bcs(identity: &IdentityKey) -> Result<Vec<u8>, NexusError> {
917 bcs::to_bytes(identity)
918 .map_err(|e| NexusError::Parsing(anyhow::anyhow!("failed to BCS-encode IdentityKey: {e}")))
919}
920
921fn sign_bytes(signing_key: &SigningKey, msg: &[u8]) -> [u8; 64] {
922 let sig: Signature = signing_key.sign(msg);
923 sig.to_bytes()
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929
930 #[test]
931 fn binding_object_id_is_deterministic_and_distinct() {
932 let mut rng = rand::thread_rng();
933 let registry_pkg_id = sui::types::Address::generate(&mut rng);
934 let network_auth_object_id = sui::types::Address::generate(&mut rng);
935 let codec = NetworkAuthCodec::new(registry_pkg_id, network_auth_object_id);
936
937 let leader = IdentityKey::leader(sui::types::Address::generate(&mut rng));
938 let tool = IdentityKey::tool(sui::types::Address::generate(&mut rng));
939
940 let leader_id_first = codec.binding_object_id(&leader).unwrap();
941 let leader_id_second = codec.binding_object_id(&leader).unwrap();
942 let tool_id = codec.binding_object_id(&tool).unwrap();
943
944 assert_eq!(leader_id_first, leader_id_second);
945 assert_ne!(leader_id_first, tool_id);
946 }
947
948 #[test]
949 fn binding_object_id_matches_move_derived_object_snapshot() {
950 let registry_pkg_id = "0x1b7beaf7c749f48e8746b2ee2803eaad6303bd353ad967c3e23db50317919beb"
951 .parse()
952 .unwrap();
953 let network_auth_object_id =
954 "0x47fc1741e0f9d0c3a8f573f82fc5c632bc3f3068c325bff24ecb76e4d685b696"
955 .parse()
956 .unwrap();
957 let leader_cap_id = "0x1b7b4eeb8a11033f52b9394b6e284abd6dc33a2a22ff18f678b65d7a909b6eb7"
958 .parse()
959 .unwrap();
960 let expected_binding_id =
961 "0xcd2e634ec159ea299824d23a437992dba70c2a2239cfb7cd16a8ee767b17c040"
962 .parse()
963 .unwrap();
964
965 let codec = NetworkAuthCodec::new(registry_pkg_id, network_auth_object_id);
966 let actual = codec
967 .binding_object_id(&IdentityKey::leader(leader_cap_id))
968 .unwrap();
969
970 assert_eq!(actual, expected_binding_id);
971 }
972
973 #[test]
974 fn pop_message_v1_matches_expected_layout() {
975 let mut rng = rand::thread_rng();
976 let identity = IdentityKey::tool(sui::types::Address::generate(&mut rng));
977 let key_id = 7u64;
978 let public_key = [9u8; 32];
979
980 let msg = pop_message_v1(&identity, key_id, public_key).unwrap();
981
982 let mut expected = Vec::new();
983 expected.extend_from_slice(POP_DOMAIN_V1);
984 expected.extend_from_slice(&identity_bcs(&identity).unwrap());
985 expected.extend_from_slice(&bcs::to_bytes(&key_id).unwrap());
986 expected.extend_from_slice(&public_key);
987
988 assert_eq!(msg, expected);
989 }
990
991 #[test]
992 fn initial_tool_registration_signs_key_zero_for_tool_identity() {
993 let signing_key = SigningKey::from_bytes(&[7u8; 32]);
994 let tool_registry_id = sui::types::Address::from_static("0x42");
995 let meta = ToolMeta {
996 fqn: "xyz.taluslabs.atomic@1".parse().unwrap(),
997 url: "https://example.com/atomic".to_string(),
998 description: "atomic".to_string(),
999 timeout: std::time::Duration::from_secs(1),
1000 input_schema: b"{}".to_vec(),
1001 output_schema: b"{}".to_vec(),
1002 };
1003
1004 let registration =
1005 initial_tool_registration(tool_registry_id, meta.clone(), &signing_key, 9).unwrap();
1006
1007 assert_eq!(registration.meta, meta);
1008 assert_eq!(registration.invocation_cost_mist, 9);
1009 assert_eq!(
1010 registration.public_key,
1011 signing_key.verifying_key().to_bytes()
1012 );
1013
1014 let tool_id = Tool::derive_id(tool_registry_id, ®istration.meta.fqn).unwrap();
1015 let identity = IdentityKey::tool(tool_id);
1016 let message = pop_message_v1(&identity, 0, registration.public_key).unwrap();
1017 let signature = Signature::from_bytes(®istration.pop_signature);
1018 signing_key
1019 .verifying_key()
1020 .verify_strict(&message, &signature)
1021 .unwrap();
1022 }
1023
1024 #[test]
1025 fn sign_bytes_produces_valid_signature() {
1026 let key = SigningKey::from_bytes(&[7u8; 32]);
1027 let msg = b"nexus";
1028 let sig = sign_bytes(&key, msg);
1029 let verify_key = key.verifying_key();
1030 let signature = Signature::from_bytes(&sig);
1031 verify_key.verify_strict(msg, &signature).unwrap();
1032 }
1033
1034 #[cfg(feature = "test_utils")]
1035 mod grpc_tests {
1036 use {
1037 super::*,
1038 crate::{
1039 move_bindings::{
1040 registry::network_auth::KeyRecord,
1041 sui_framework::{object::UID, table::Table as MoveTable, vec_set::VecSet},
1042 },
1043 test_utils::{nexus_mocks, sui_mocks},
1044 },
1045 serde::Serialize,
1046 tonic::Response,
1047 };
1048
1049 #[derive(Clone, Debug, Serialize)]
1050 struct DynamicFieldValueBcs<K, V> {
1051 id: sui::types::Address,
1052 name: K,
1053 value: V,
1054 }
1055
1056 fn raw_network_auth_for_test(
1057 id: sui::types::Address,
1058 identities: Vec<IdentityKey>,
1059 ) -> (NetworkAuth, NetworkAuthInnerV1) {
1060 (
1061 NetworkAuth::new(UID::new(id)),
1062 NetworkAuthInnerV1::new(
1063 UID::new(sui::types::Address::from_static("0x123")),
1064 VecSet {
1065 contents: identities,
1066 },
1067 ),
1068 )
1069 }
1070
1071 fn raw_key_binding_for_test(
1072 id: sui::types::Address,
1073 identity: IdentityKey,
1074 next_key_id: u64,
1075 active_key_id: Option<u64>,
1076 keys: MoveTable<u64, KeyRecord>,
1077 ) -> (KeyBinding, KeyBindingInnerV1) {
1078 (
1079 KeyBinding::new(UID::new(id)),
1080 KeyBindingInnerV1::new_for_test(identity, None, next_key_id, active_key_id, keys),
1081 )
1082 }
1083
1084 fn standalone_context(
1085 registry_package: sui::types::Address,
1086 network_auth: sui::types::Address,
1087 ) -> NexusContext {
1088 let mut objects = sui_mocks::mock_nexus_objects();
1089 objects.network_auth = crate::types::SharedRoot::new(network_auth, 1);
1090 let mut packages = sui_mocks::mock_nexus_packages();
1091 let registry = packages.registry.as_mut().unwrap();
1092 let previous_origin = registry.initial_id;
1093 registry.initial_id = registry_package;
1094 registry.storage_id = registry_package;
1095 for datatypes in registry.type_origins.values_mut() {
1096 for origin in datatypes.values_mut() {
1097 if *origin == previous_origin {
1098 *origin = registry_package;
1099 }
1100 }
1101 }
1102 NexusContext::new(Arc::new(objects), packages)
1103 }
1104
1105 fn owner_immutable() -> sui::grpc::Owner {
1106 let mut owner = sui::grpc::Owner::default();
1107 owner.kind = Some(sui::grpc::owner::OwnerKind::Immutable as i32);
1108 owner
1109 }
1110
1111 fn object_with_contents(
1112 object_id: Option<sui::types::Address>,
1113 contents: Vec<u8>,
1114 ) -> sui::grpc::Object {
1115 let mut rng = rand::thread_rng();
1116 let digest = sui::types::Digest::generate(&mut rng);
1117 let mut object = sui::grpc::Object::default();
1118 object.object_id = object_id.map(|id| id.to_string());
1119 object.owner = Some(owner_immutable());
1120 object.digest = Some(digest.to_string());
1121 object.version = Some(1);
1122 let mut bcs = sui::grpc::Bcs::default();
1123 bcs.value = Some(contents.into());
1124 object.contents = Some(bcs);
1125 object
1126 }
1127
1128 fn mock_key_record_field(
1129 ledger_service: &mut sui_mocks::grpc::MockLedgerService,
1130 key_table_id: sui::types::Address,
1131 key_id: u64,
1132 record: KeyRecord,
1133 times: usize,
1134 ) {
1135 let field_id = key_table_id.derive_dynamic_child_id(
1136 &sui::types::TypeTag::U64,
1137 &bcs::to_bytes(&key_id).expect("key ID serializes"),
1138 );
1139 let field = DynamicFieldValueBcs {
1140 id: field_id,
1141 name: key_id,
1142 value: record,
1143 };
1144 let field_bytes = bcs::to_bytes(&field).expect("key record field serializes");
1145
1146 for _ in 0..times {
1147 sui_mocks::grpc::mock_get_object_bcs(
1148 ledger_service,
1149 sui_mocks::object_ref_for_id(field_id),
1150 sui::types::Owner::Object(key_table_id),
1151 field_bytes.clone(),
1152 );
1153 }
1154 }
1155
1156 async fn build_reader(
1157 registry_pkg_id: sui::types::Address,
1158 network_auth_object_id: sui::types::Address,
1159 leader_cap_id: sui::types::Address,
1160 active_kid: u64,
1161 record: KeyRecord,
1162 ) -> NetworkAuthReader {
1163 let codec = NetworkAuthCodec::new(registry_pkg_id, network_auth_object_id);
1164 let identity = IdentityKey::leader(leader_cap_id);
1165 let binding_object_id = codec.binding_object_id(&identity).unwrap();
1166
1167 let key_table_id = sui::types::Address::from_static("0x111");
1168 let (binding, binding_state) = raw_key_binding_for_test(
1169 binding_object_id,
1170 identity,
1171 active_kid + 1,
1172 Some(active_kid),
1173 MoveTable::new(key_table_id, 1),
1174 );
1175
1176 let context = standalone_context(registry_pkg_id, network_auth_object_id);
1177 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
1178 let mut package_service = sui_mocks::grpc::MockMovePackageService::new();
1179 let mut state_service = sui_mocks::grpc::MockStateService::new();
1180 sui_mocks::grpc::mock_object_state::<KeyBinding, RegistryWitnessV1, KeyBindingInnerV1>(
1181 &mut ledger_service,
1182 &mut state_service,
1183 &context,
1184 sui_mocks::object_ref_for_id(binding_object_id),
1185 sui::types::Owner::Shared(1),
1186 binding,
1187 binding_state,
1188 );
1189 sui_mocks::grpc::mock_nexus_package_graph(
1190 &mut ledger_service,
1191 &mut package_service,
1192 context.packages(),
1193 );
1194
1195 mock_key_record_field(&mut ledger_service, key_table_id, active_kid, record, 1);
1196
1197 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
1198 ledger_service_mock: Some(ledger_service),
1199 package_service_mock: Some(package_service),
1200 state_service_mock: Some(state_service),
1201 ..Default::default()
1202 });
1203
1204 NetworkAuthReader::from_rpc_url(&rpc_url, registry_pkg_id, network_auth_object_id)
1205 .unwrap()
1206 }
1207
1208 #[cfg(feature = "signed_http")]
1209 async fn build_reader_with_network_auth(
1210 registry_pkg_id: sui::types::Address,
1211 network_auth_object_id: sui::types::Address,
1212 leader_cap_id: sui::types::Address,
1213 active_kid: u64,
1214 record: KeyRecord,
1215 ) -> NetworkAuthReader {
1216 let codec = NetworkAuthCodec::new(registry_pkg_id, network_auth_object_id);
1217 let identity = IdentityKey::leader(leader_cap_id);
1218 let binding_object_id = codec.binding_object_id(&identity).unwrap();
1219
1220 let key_table_id = sui::types::Address::from_static("0x111");
1221 let (binding, binding_state) = raw_key_binding_for_test(
1222 binding_object_id,
1223 identity.clone(),
1224 active_kid + 1,
1225 Some(active_kid),
1226 MoveTable::new(key_table_id, 1),
1227 );
1228
1229 let (network_auth, network_auth_state) = raw_network_auth_for_test(
1230 network_auth_object_id,
1231 vec![
1232 identity.clone(),
1233 IdentityKey::tool(sui::types::Address::from_static("0x42")),
1234 ],
1235 );
1236
1237 let context = standalone_context(registry_pkg_id, network_auth_object_id);
1238 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
1239 let mut package_service = sui_mocks::grpc::MockMovePackageService::new();
1240 let mut state_service = sui_mocks::grpc::MockStateService::new();
1241 sui_mocks::grpc::mock_object_state::<NetworkAuth, RegistryWitnessV1, NetworkAuthInnerV1>(
1242 &mut ledger_service,
1243 &mut state_service,
1244 &context,
1245 sui_mocks::object_ref_for_id(network_auth_object_id),
1246 sui::types::Owner::Shared(1),
1247 network_auth,
1248 network_auth_state,
1249 );
1250 sui_mocks::grpc::mock_object_state::<KeyBinding, RegistryWitnessV1, KeyBindingInnerV1>(
1251 &mut ledger_service,
1252 &mut state_service,
1253 &context,
1254 sui_mocks::object_ref_for_id(binding_object_id),
1255 sui::types::Owner::Shared(1),
1256 binding,
1257 binding_state,
1258 );
1259 sui_mocks::grpc::mock_nexus_package_graph(
1260 &mut ledger_service,
1261 &mut package_service,
1262 context.packages(),
1263 );
1264
1265 mock_key_record_field(&mut ledger_service, key_table_id, active_kid, record, 1);
1266
1267 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
1268 ledger_service_mock: Some(ledger_service),
1269 package_service_mock: Some(package_service),
1270 state_service_mock: Some(state_service),
1271 ..Default::default()
1272 });
1273
1274 NetworkAuthReader::from_rpc_url(&rpc_url, registry_pkg_id, network_auth_object_id)
1275 .unwrap()
1276 }
1277
1278 #[tokio::test]
1279 async fn reader_try_get_active_key_binding_returns_validated_active_key() {
1280 let mut rng = rand::thread_rng();
1281 let registry_pkg_id = sui::types::Address::generate(&mut rng);
1282 let network_auth_object_id = sui::types::Address::generate(&mut rng);
1283 let leader_cap_id = sui::types::Address::generate(&mut rng);
1284 let active_kid = 5u64;
1285 let public_key = [7u8; 32];
1286 let reader = build_reader(
1287 registry_pkg_id,
1288 network_auth_object_id,
1289 leader_cap_id,
1290 active_kid,
1291 KeyRecord::new_for_test(0, public_key.to_vec(), 0, None),
1292 )
1293 .await;
1294
1295 let identity = IdentityKey::leader(leader_cap_id);
1296 let resolved = reader
1297 .try_get_active_key_binding(&identity)
1298 .await
1299 .unwrap()
1300 .expect("binding should exist");
1301
1302 assert_eq!(
1303 reader.binding_object_id(&identity).unwrap(),
1304 resolved.binding.object_id
1305 );
1306 assert_eq!(
1307 resolved.active_key,
1308 Some(ActiveEd25519Key {
1309 kid: active_kid,
1310 public_key,
1311 })
1312 );
1313 }
1314
1315 #[cfg(feature = "signed_http")]
1316 #[tokio::test]
1317 async fn actions_export_allowlists() {
1318 let mut rng = rand::thread_rng();
1319 let network_auth_object_id = sui::types::Address::generate(&mut rng);
1320 let leader_cap_id = sui::types::Address::generate(&mut rng);
1321 let mut nexus_objects = sui_mocks::mock_nexus_objects();
1322 nexus_objects.network_auth = crate::types::SharedRoot::new(network_auth_object_id, 1);
1323 let context = sui_mocks::mock_nexus_context_for(&nexus_objects);
1324 let registry_pkg_id = context
1325 .require_package(PackageRole::Registry)
1326 .unwrap()
1327 .initial_id;
1328
1329 let codec = NetworkAuthCodec::new(registry_pkg_id, network_auth_object_id);
1330 let identity = IdentityKey::leader(leader_cap_id);
1331 let binding_object_id = codec.binding_object_id(&identity).unwrap();
1332
1333 let active_kid = 3u64;
1334 let public_key = [7u8; 32];
1335 let record = KeyRecord::new_for_test(0, public_key.to_vec(), 0, None);
1336
1337 let key_table_id = sui::types::Address::from_static("0x111");
1338 let (binding, binding_state) = raw_key_binding_for_test(
1339 binding_object_id,
1340 identity.clone(),
1341 active_kid + 1,
1342 Some(active_kid),
1343 MoveTable::new(key_table_id, 1),
1344 );
1345
1346 let (network_auth, network_auth_state) = raw_network_auth_for_test(
1347 network_auth_object_id,
1348 vec![
1349 identity.clone(),
1350 IdentityKey::tool(sui::types::Address::from_static("0x42")),
1351 ],
1352 );
1353
1354 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
1355 let mut package_service = sui_mocks::grpc::MockMovePackageService::new();
1356 let mut state_service = sui_mocks::grpc::MockStateService::new();
1357 sui_mocks::grpc::mock_object_state::<NetworkAuth, RegistryWitnessV1, NetworkAuthInnerV1>(
1358 &mut ledger_service,
1359 &mut state_service,
1360 &context,
1361 sui_mocks::object_ref_for_id(network_auth_object_id),
1362 sui::types::Owner::Shared(1),
1363 network_auth,
1364 network_auth_state,
1365 );
1366 sui_mocks::grpc::mock_object_state::<KeyBinding, RegistryWitnessV1, KeyBindingInnerV1>(
1367 &mut ledger_service,
1368 &mut state_service,
1369 &context,
1370 sui_mocks::object_ref_for_id(binding_object_id),
1371 sui::types::Owner::Shared(1),
1372 binding,
1373 binding_state,
1374 );
1375 sui_mocks::grpc::mock_nexus_package_graph(
1376 &mut ledger_service,
1377 &mut package_service,
1378 context.packages(),
1379 );
1380
1381 mock_key_record_field(&mut ledger_service, key_table_id, active_kid, record, 2);
1382
1383 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
1384 ledger_service_mock: Some(ledger_service),
1385 package_service_mock: Some(package_service),
1386 state_service_mock: Some(state_service),
1387 ..Default::default()
1388 });
1389
1390 let client =
1391 nexus_mocks::mock_nexus_client_without_coins(&nexus_objects, &rpc_url).await;
1392
1393 let expected_entry = AllowedLeaderFileV1 {
1394 leader_id: leader_cap_id.to_string(),
1395 keys: vec![AllowedLeaderKeyFileV1 {
1396 kid: active_kid,
1397 public_key: hex::encode(public_key),
1398 }],
1399 };
1400
1401 let file = client
1402 .network_auth()
1403 .export_allowed_leaders_file_v1(&[leader_cap_id])
1404 .await
1405 .unwrap();
1406 assert_eq!(file.leaders.len(), 1);
1407 assert_eq!(file.leaders[0].leader_id, expected_entry.leader_id);
1408 assert_eq!(file.leaders[0].keys.len(), 1);
1409 assert_eq!(file.leaders[0].keys[0].kid, active_kid);
1410 assert_eq!(file.leaders[0].keys[0].public_key, hex::encode(public_key));
1411
1412 let leaders = client
1413 .network_auth()
1414 .list_leader_cap_ids_from_network_auth()
1415 .await
1416 .unwrap();
1417 assert_eq!(leaders, vec![leader_cap_id]);
1418
1419 let file = client
1420 .network_auth()
1421 .export_allowed_leaders_file_v1_for_all_leaders()
1422 .await
1423 .unwrap();
1424 assert_eq!(file.leaders.len(), 1);
1425 assert_eq!(file.leaders[0].leader_id, expected_entry.leader_id);
1426 assert_eq!(file.leaders[0].keys.len(), 1);
1427 assert_eq!(file.leaders[0].keys[0].kid, active_kid);
1428 assert_eq!(file.leaders[0].keys[0].public_key, hex::encode(public_key));
1429 }
1430
1431 #[cfg(feature = "signed_http")]
1432 #[tokio::test]
1433 async fn reader_export_allowed_leaders_file_v1_for_all_leaders() {
1434 let mut rng = rand::thread_rng();
1435 let registry_pkg_id = sui::types::Address::generate(&mut rng);
1436 let network_auth_object_id = sui::types::Address::generate(&mut rng);
1437 let leader_cap_id = sui::types::Address::generate(&mut rng);
1438
1439 let active_kid = 7u64;
1440 let public_key = [9u8; 32];
1441
1442 let reader = build_reader_with_network_auth(
1443 registry_pkg_id,
1444 network_auth_object_id,
1445 leader_cap_id,
1446 active_kid,
1447 KeyRecord::new_for_test(0, public_key.to_vec(), 0, None),
1448 )
1449 .await;
1450
1451 let allowlist = reader
1452 .export_allowed_leaders_file_v1_for_all_leaders()
1453 .await
1454 .unwrap();
1455 assert_eq!(allowlist.version, 1);
1456 assert_eq!(allowlist.leaders.len(), 1);
1457 assert_eq!(allowlist.leaders[0].leader_id, leader_cap_id.to_string());
1458 assert_eq!(allowlist.leaders[0].keys.len(), 1);
1459 assert_eq!(allowlist.leaders[0].keys[0].kid, active_kid);
1460 assert_eq!(
1461 allowlist.leaders[0].keys[0].public_key,
1462 hex::encode(public_key)
1463 );
1464 }
1465
1466 #[tokio::test]
1471 async fn list_tool_keys_returns_sorted_entries() {
1472 let mut rng = rand::thread_rng();
1473 let network_auth_object_id = sui::types::Address::generate(&mut rng);
1474
1475 let tool_fqn_str = "xyz.demo.tool@1";
1476 let tool_fqn: crate::ToolFqn = tool_fqn_str.parse().unwrap();
1477 let tool_registry_id = sui::types::Address::generate(&mut rng);
1478 let mut nexus_objects = sui_mocks::mock_nexus_objects();
1479 nexus_objects.network_auth = crate::types::SharedRoot::new(network_auth_object_id, 1);
1480 nexus_objects.tool_registry = crate::types::SharedRoot::new(tool_registry_id, 1);
1481 let context = sui_mocks::mock_nexus_context_for(&nexus_objects);
1482 let registry_pkg_id = context
1483 .require_package(PackageRole::Registry)
1484 .unwrap()
1485 .initial_id;
1486
1487 let codec = NetworkAuthCodec::new(registry_pkg_id, network_auth_object_id);
1488 let identity = IdentityKey::tool(
1489 Tool::derive_id(tool_registry_id, &tool_fqn).expect("Tool ID derives"),
1490 );
1491 let binding_object_id = codec.binding_object_id(&identity).unwrap();
1492
1493 let key_table_id = sui::types::Address::from_static("0x111");
1494
1495 let record_0 = KeyRecord::new_for_test(0, vec![0xaau8; 32], 1000, Some(2000));
1497 let record_1 = KeyRecord::new_for_test(0, vec![0xbbu8; 32], 3000, None);
1498
1499 let (binding, binding_state) = raw_key_binding_for_test(
1500 binding_object_id,
1501 identity.clone(),
1502 2,
1503 Some(1),
1504 MoveTable::new(key_table_id, 2),
1505 );
1506
1507 let field_0_id = sui::types::Address::from_static("0x333");
1508 let field_1_id = sui::types::Address::from_static("0x444");
1509
1510 let field_0_value = DynamicFieldValueBcs {
1511 id: sui::types::Address::from_static("0x555"),
1512 name: 0u64,
1513 value: record_0,
1514 };
1515 let field_1_value = DynamicFieldValueBcs {
1516 id: sui::types::Address::from_static("0x666"),
1517 name: 1u64,
1518 value: record_1,
1519 };
1520 let field_0_bytes = bcs::to_bytes(&field_0_value).unwrap();
1521 let field_1_bytes = bcs::to_bytes(&field_1_value).unwrap();
1522
1523 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
1524 let mut package_service = sui_mocks::grpc::MockMovePackageService::new();
1525 let mut state_service = sui_mocks::grpc::MockStateService::new();
1526 let (network_auth, network_auth_state) =
1527 raw_network_auth_for_test(network_auth_object_id, vec![]);
1528 sui_mocks::grpc::mock_object_state::<NetworkAuth, RegistryWitnessV1, NetworkAuthInnerV1>(
1529 &mut ledger_service,
1530 &mut state_service,
1531 &context,
1532 sui_mocks::object_ref_for_id(network_auth_object_id),
1533 sui::types::Owner::Shared(1),
1534 network_auth,
1535 network_auth_state,
1536 );
1537
1538 sui_mocks::grpc::mock_object_state::<KeyBinding, RegistryWitnessV1, KeyBindingInnerV1>(
1539 &mut ledger_service,
1540 &mut state_service,
1541 &context,
1542 sui_mocks::object_ref_for_id(binding_object_id),
1543 sui::types::Owner::Shared(1),
1544 binding,
1545 binding_state,
1546 );
1547 sui_mocks::grpc::mock_nexus_package_graph(
1548 &mut ledger_service,
1549 &mut package_service,
1550 context.packages(),
1551 );
1552
1553 let expected_key_table = key_table_id.to_string();
1556 state_service
1557 .expect_list_dynamic_fields()
1558 .withf(move |request| {
1559 request.get_ref().parent.as_deref() == Some(expected_key_table.as_str())
1560 })
1561 .times(1)
1562 .returning(move |_request| {
1563 let mut df1 = sui::grpc::DynamicField::default();
1564 df1.set_child_id(field_1_id);
1565 df1.set_field_id(field_1_id);
1566 let mut name1 = sui::grpc::Bcs::default();
1567 name1.value = Some(bcs::to_bytes(&1u64).unwrap().into());
1568 df1.set_name(name1);
1569
1570 let mut df0 = sui::grpc::DynamicField::default();
1571 df0.set_child_id(field_0_id);
1572 df0.set_field_id(field_0_id);
1573 let mut name0 = sui::grpc::Bcs::default();
1574 name0.value = Some(bcs::to_bytes(&0u64).unwrap().into());
1575 df0.set_name(name0);
1576
1577 let mut response = sui::grpc::ListDynamicFieldsResponse::default();
1578 response.dynamic_fields = vec![df1, df0];
1579 Ok(Response::new(response))
1580 });
1581
1582 ledger_service
1584 .expect_batch_get_objects()
1585 .times(1)
1586 .returning(move |_request| {
1587 let obj1 = object_with_contents(Some(field_1_id), field_1_bytes.clone());
1588 let mut r1 = sui::grpc::GetObjectResult::default();
1589 r1.result = Some(sui::grpc::get_object_result::Result::Object(obj1));
1590
1591 let obj0 = object_with_contents(Some(field_0_id), field_0_bytes.clone());
1592 let mut r0 = sui::grpc::GetObjectResult::default();
1593 r0.result = Some(sui::grpc::get_object_result::Result::Object(obj0));
1594
1595 let mut response = sui::grpc::BatchGetObjectsResponse::default();
1596 response.objects = vec![r1, r0];
1597 Ok(Response::new(response))
1598 });
1599
1600 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
1601 ledger_service_mock: Some(ledger_service),
1602 package_service_mock: Some(package_service),
1603 state_service_mock: Some(state_service),
1604 ..Default::default()
1605 });
1606
1607 let client =
1608 nexus_mocks::mock_nexus_client_without_coins(&nexus_objects, &rpc_url).await;
1609
1610 let list = client
1611 .network_auth()
1612 .list_tool_keys(&tool_fqn)
1613 .await
1614 .unwrap()
1615 .expect("binding exists, should return Some");
1616
1617 assert_eq!(list.binding_object_id, binding_object_id);
1618 assert_eq!(list.active_key_id, Some(1));
1619 assert_eq!(list.next_key_id, 2);
1620 assert_eq!(list.keys.len(), 2);
1621
1622 assert_eq!(list.keys[0].kid, 0);
1624 assert_eq!(list.keys[0].public_key_hex, hex::encode([0xaau8; 32]));
1625 assert_eq!(list.keys[0].added_at_ms, 1000);
1626 assert!(list.keys[0].revoked);
1627
1628 assert_eq!(list.keys[1].kid, 1);
1629 assert_eq!(list.keys[1].public_key_hex, hex::encode([0xbbu8; 32]));
1630 assert_eq!(list.keys[1].added_at_ms, 3000);
1631 assert!(!list.keys[1].revoked);
1632 }
1633 }
1634}