1use crate::{
5 error::{AuthenticatorError, PollResult},
6 init::InitializingAuthenticator,
7};
8
9use std::sync::Arc;
10
11use crate::{
12 api_types::{
13 AccountInclusionProof, GatewayRequestState, IndexerAuthenticatorPubkeysResponse,
14 IndexerErrorCode, IndexerPackedAccountRequest, IndexerPackedAccountResponse,
15 IndexerQueryRequest, IndexerSignatureNonceResponse, ServiceApiError,
16 },
17 service_client::{ServiceClient, ServiceKind},
18};
19use world_id_primitives::{Credential, FieldElement, ProofResponse, Signer};
20
21pub use crate::ohttp::OhttpClientConfig;
22use alloy::{
23 primitives::Address,
24 providers::DynProvider,
25 signers::{Signature, SignerSync},
26};
27use ark_serialize::CanonicalSerialize;
28use eddsa_babyjubjub::EdDSAPublicKey;
29use groth16_material::circom::CircomGroth16Material;
30use ruint::{aliases::U256, uint};
31use taceo_oprf::client::Connector;
32use world_id_primitives::{
33 AuthenticatorPublicKeySet, PrimitiveError, SparseAuthenticatorPubkeysError,
34};
35pub use world_id_primitives::{Config, ServiceEndpoint, TREE_DEPTH, authenticator::ProtocolSigner};
36use world_id_registries::world_id::WorldIdRegistry::WorldIdRegistryInstance;
37
38#[expect(unused_imports, reason = "used for docs")]
39use world_id_primitives::{Nullifier, SessionId};
40
41static MASK_RECOVERY_COUNTER: U256 =
42 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
43static MASK_PUBKEY_ID: U256 =
44 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
45static MASK_LEAF_INDEX: U256 =
46 uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
47
48pub struct CredentialInput {
50 pub credential: Credential,
52 pub blinding_factor: FieldElement,
54}
55
56#[derive(Debug)]
61pub struct ProofResult {
62 pub session_id_r_seed: Option<FieldElement>,
66
67 pub proof_response: ProofResponse,
69}
70
71pub struct Authenticator {
83 pub config: Config,
85 pub packed_account_data: U256,
88 pub(crate) signer: Signer,
89 pub(crate) registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
90 pub(crate) indexer_client: ServiceClient,
91 pub(crate) gateway_client: ServiceClient,
92 pub(crate) ws_connector: Connector,
93 pub(crate) query_material: Option<Arc<CircomGroth16Material>>,
94 pub(crate) nullifier_material: Option<Arc<CircomGroth16Material>>,
95}
96
97impl std::fmt::Debug for Authenticator {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("Authenticator")
101 .field("config", &self.config)
102 .finish_non_exhaustive()
103 }
104}
105
106impl Authenticator {
107 pub async fn init(seed: &[u8], config: Config) -> Result<Self, AuthenticatorError> {
130 let signer = Signer::from_seed_bytes(seed)?;
131
132 let registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>> =
133 config.rpc_url().map(|rpc_url| {
134 let provider = alloy::providers::ProviderBuilder::new()
135 .with_chain_id(config.chain_id())
136 .connect_http(rpc_url.clone());
137 Arc::new(world_id_registries::world_id::WorldIdRegistry::new(
138 *config.registry_address(),
139 alloy::providers::Provider::erased(provider),
140 ))
141 });
142
143 let http_client = reqwest::Client::new();
144
145 let indexer_client =
146 ServiceClient::new(http_client.clone(), ServiceKind::Indexer, config.indexer())?;
147
148 let gateway_client =
149 ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())?;
150
151 let packed_account_data = Self::fetch_packed_account_data_for(
152 signer.onchain_signer_address(),
153 registry.as_deref(),
154 &config,
155 &indexer_client,
156 )
157 .await?;
158
159 #[cfg(not(target_arch = "wasm32"))]
160 let ws_connector = {
161 let mut root_store = rustls::RootCertStore::empty();
162 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
163 let rustls_config = rustls::ClientConfig::builder()
164 .with_root_certificates(root_store)
165 .with_no_client_auth();
166 Connector::Rustls(Arc::new(rustls_config))
167 };
168
169 #[cfg(target_arch = "wasm32")]
170 let ws_connector = Connector;
171
172 Ok(Self {
173 packed_account_data,
174 signer,
175 config,
176 registry,
177 indexer_client,
178 gateway_client,
179 ws_connector,
180 query_material: None,
181 nullifier_material: None,
182 })
183 }
184
185 #[must_use]
190 pub fn with_proof_materials(
191 self,
192 query_material: Arc<CircomGroth16Material>,
193 nullifier_material: Arc<CircomGroth16Material>,
194 ) -> Self {
195 Self {
196 query_material: Some(query_material),
197 nullifier_material: Some(nullifier_material),
198 ..self
199 }
200 }
201
202 pub async fn register(
210 seed: &[u8],
211 config: Config,
212 recovery_address: Option<Address>,
213 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
214 let gateway_client = ServiceClient::new(
215 reqwest::Client::new(),
216 ServiceKind::Gateway,
217 config.gateway(),
218 )?;
219 InitializingAuthenticator::new(seed, config, recovery_address, gateway_client).await
220 }
221
222 pub async fn init_or_register(
235 seed: &[u8],
236 config: Config,
237 recovery_address: Option<Address>,
238 ) -> Result<Self, AuthenticatorError> {
239 match Self::init(seed, config.clone()).await {
240 Ok(authenticator) => Ok(authenticator),
241 Err(AuthenticatorError::AccountDoesNotExist) => {
242 let gateway_client = ServiceClient::new(
243 reqwest::Client::new(),
244 ServiceKind::Gateway,
245 config.gateway(),
246 )?;
247 let initializing_authenticator = InitializingAuthenticator::new(
248 seed,
249 config.clone(),
250 recovery_address,
251 gateway_client,
252 )
253 .await?;
254
255 let backoff = backon::ExponentialBuilder::default()
256 .with_min_delay(std::time::Duration::from_millis(800))
257 .with_factor(1.5)
258 .without_max_times()
259 .with_total_delay(Some(std::time::Duration::from_secs(120)));
260
261 let poller = || async {
262 let poll_status = initializing_authenticator.poll_status().await;
263 let result = match poll_status {
264 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
265 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
266 PollResult::TerminalError(AuthenticatorError::RegistrationError {
267 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
268 error_message: error,
269 }),
270 ),
271 Err(AuthenticatorError::GatewayError { status, body }) => {
272 if status.is_client_error() {
273 Err(PollResult::TerminalError(
274 AuthenticatorError::GatewayError { status, body },
275 ))
276 } else {
277 Err(PollResult::Retryable)
278 }
279 }
280 _ => Err(PollResult::Retryable),
281 };
282
283 match result {
284 Ok(()) => match Self::init(seed, config.clone()).await {
285 Ok(auth) => Ok(auth),
286 Err(AuthenticatorError::AccountDoesNotExist) => {
287 Err(PollResult::Retryable)
288 }
289 Err(e) => Err(PollResult::TerminalError(e)),
290 },
291 Err(e) => Err(e),
292 }
293 };
294
295 let result = backon::Retryable::retry(poller, backoff)
296 .when(|e| matches!(e, PollResult::Retryable))
297 .await;
298
299 match result {
300 Ok(authenticator) => Ok(authenticator),
301 Err(PollResult::TerminalError(e)) => Err(e),
302 Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
303 }
304 }
305 Err(e) => Err(e),
306 }
307 }
308
309 pub async fn fetch_packed_account_data(&self) -> Result<U256, AuthenticatorError> {
315 Self::fetch_packed_account_data_for(
316 self.onchain_address(),
317 self.registry().as_deref(),
318 &self.config,
319 &self.indexer_client,
320 )
321 .await
322 }
323
324 pub async fn refresh_packed_account_data(&mut self) -> Result<U256, AuthenticatorError> {
329 let packed_account_data = self.fetch_packed_account_data().await?;
330 self.packed_account_data = packed_account_data;
331 Ok(packed_account_data)
332 }
333
334 async fn fetch_packed_account_data_for(
342 onchain_signer_address: Address,
343 registry: Option<&WorldIdRegistryInstance<DynProvider>>,
344 config: &Config,
345 indexer_client: &ServiceClient,
346 ) -> Result<U256, AuthenticatorError> {
347 let raw_index = if let Some(registry) = registry {
349 registry
351 .getPackedAccountData(onchain_signer_address)
352 .call()
353 .await?
354 } else {
355 let req = IndexerPackedAccountRequest {
356 authenticator_address: onchain_signer_address,
357 };
358 match indexer_client
359 .post_json::<_, IndexerPackedAccountResponse>(
360 config.indexer_url(),
361 "/packed-account",
362 &req,
363 )
364 .await
365 {
366 Ok(response) => response.packed_account_data,
367 Err(AuthenticatorError::IndexerError { status, body }) => {
368 if let Ok(error_resp) =
369 serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
370 {
371 return match error_resp.code {
372 IndexerErrorCode::AccountDoesNotExist => {
373 Err(AuthenticatorError::AccountDoesNotExist)
374 }
375 _ => Err(AuthenticatorError::IndexerError {
376 status,
377 body: error_resp.message,
378 }),
379 };
380 }
381
382 return Err(AuthenticatorError::IndexerError { status, body });
383 }
384 Err(other) => return Err(other),
385 }
386 };
387
388 if raw_index == U256::ZERO {
389 return Err(AuthenticatorError::AccountDoesNotExist);
390 }
391
392 Ok(raw_index)
393 }
394
395 #[must_use]
398 pub const fn onchain_address(&self) -> Address {
399 self.signer.onchain_signer_address()
400 }
401
402 #[must_use]
405 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
406 self.signer.offchain_signer_pubkey()
407 }
408
409 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
414 let pk = self.signer.offchain_signer_pubkey().pk;
415 let mut compressed_bytes = Vec::new();
416 pk.serialize_compressed(&mut compressed_bytes)
417 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
418 Ok(U256::from_le_slice(&compressed_bytes))
419 }
420
421 #[must_use]
423 pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
424 self.registry.clone()
425 }
426
427 #[must_use]
443 pub fn leaf_index(&self) -> u64 {
444 (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
445 }
446
447 #[must_use]
451 pub fn recovery_counter(&self) -> U256 {
452 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
453 recovery_counter >> 224
454 }
455
456 #[must_use]
460 pub fn pubkey_id(&self) -> U256 {
461 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
462 pubkey_id >> 192
463 }
464
465 pub async fn fetch_inclusion_proof(
471 &self,
472 ) -> Result<AccountInclusionProof<TREE_DEPTH>, AuthenticatorError> {
473 let req = IndexerQueryRequest {
474 leaf_index: self.leaf_index(),
475 };
476 let response: AccountInclusionProof<TREE_DEPTH> = self
477 .indexer_client
478 .post_json(self.config.indexer_url(), "/inclusion-proof", &req)
479 .await?;
480
481 Ok(response)
482 }
483
484 pub async fn fetch_authenticator_pubkeys(
493 &self,
494 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
495 let req = IndexerQueryRequest {
496 leaf_index: self.leaf_index(),
497 };
498 let response: IndexerAuthenticatorPubkeysResponse = self
499 .indexer_client
500 .post_json(self.config.indexer_url(), "/authenticator-pubkeys", &req)
501 .await?;
502 Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
503 }
504
505 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
510 let registry = self.registry();
511 if let Some(registry) = registry {
512 let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
513 Ok(nonce)
514 } else {
515 let req = IndexerQueryRequest {
516 leaf_index: self.leaf_index(),
517 };
518 let response: IndexerSignatureNonceResponse = self
519 .indexer_client
520 .post_json(self.config.indexer_url(), "/signature-nonce", &req)
521 .await?;
522 Ok(response.signature_nonce)
523 }
524 }
525
526 pub fn danger_sign_challenge(&self, challenge: &[u8]) -> Result<Signature, AuthenticatorError> {
538 self.signer
539 .onchain_signer()
540 .sign_message_sync(challenge)
541 .map_err(|e| AuthenticatorError::Generic(format!("signature error: {e}")))
542 }
543
544 pub(crate) fn decode_indexer_pubkeys(
545 pubkeys: Vec<Option<U256>>,
546 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
547 AuthenticatorPublicKeySet::from_sparse_encoded_pubkeys(pubkeys).map_err(|e| match e {
548 SparseAuthenticatorPubkeysError::SlotOutOfBounds {
549 slot_index,
550 max_supported_slot,
551 } => AuthenticatorError::InvalidIndexerPubkeySlot {
552 slot_index,
553 max_supported_slot,
554 },
555 SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
556 PrimitiveError::Deserialization(format!(
557 "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
558 ))
559 .into()
560 }
561 })
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::{error::AuthenticatorError, traits::OnchainKeyRepresentable};
569 use alloy::primitives::{U256, address};
570 use world_id_primitives::MAX_AUTHENTICATOR_KEYS;
571
572 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
573 Signer::from_seed_bytes(&[seed_byte; 32])
574 .unwrap()
575 .offchain_signer_pubkey()
576 }
577
578 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
579 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
580 }
581
582 #[test]
583 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
584 let mut key_set =
585 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
586 .unwrap();
587 key_set[1] = None;
588 let new_key = test_pubkey(3);
589
590 let index = key_set.insert_or_reuse(new_key).unwrap();
591
592 assert_eq!(index, 1);
593 assert_eq!(key_set.len(), 3);
594 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
595 }
596
597 #[test]
598 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
599 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
600 let new_key = test_pubkey(2);
601
602 let index = key_set.insert_or_reuse(new_key).unwrap();
603
604 assert_eq!(index, 1);
605 assert_eq!(key_set.len(), 2);
606 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
607 }
608
609 #[test]
610 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
611 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
612 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
613
614 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
615
616 assert_eq!(key_set.len(), 2);
617 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
618 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
619 }
620
621 #[test]
622 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
623 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
624 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
625
626 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
627 assert!(matches!(
628 error,
629 AuthenticatorError::InvalidIndexerPubkeySlot {
630 slot_index,
631 max_supported_slot
632 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
633 ));
634 }
635
636 #[tokio::test]
637 async fn test_get_packed_account_data_from_indexer() {
638 let mut server = mockito::Server::new_async().await;
639 let indexer_url = server.url();
640 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
641 let expected_packed_index = U256::from(42);
642 let mock = server
643 .mock("POST", "/packed-account")
644 .match_header("content-type", "application/json")
645 .match_body(mockito::Matcher::JsonString(
646 serde_json::json!({ "authenticator_address": test_address }).to_string(),
647 ))
648 .with_status(200)
649 .with_header("content-type", "application/json")
650 .with_body(
651 serde_json::json!({ "packed_account_data": format!("{:#x}", expected_packed_index) }).to_string(),
652 )
653 .create_async()
654 .await;
655 let config = Config::new(
656 None,
657 1,
658 address!("0x0000000000000000000000000000000000000001"),
659 ServiceEndpoint::direct(indexer_url),
660 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
661 Vec::new(),
662 2,
663 )
664 .unwrap();
665
666 let indexer_client = ServiceClient::new(
667 reqwest::Client::new(),
668 ServiceKind::Indexer,
669 config.indexer(),
670 )
671 .unwrap();
672
673 let result = Authenticator::fetch_packed_account_data_for(
674 test_address,
675 None, &config,
677 &indexer_client,
678 )
679 .await
680 .unwrap();
681
682 assert_eq!(result, expected_packed_index);
683 mock.assert_async().await;
684 drop(server);
685 }
686
687 #[tokio::test]
688 async fn test_get_packed_account_data_from_indexer_error() {
689 let mut server = mockito::Server::new_async().await;
690 let indexer_url = server.url();
691 let test_address = address!("0x0000000000000000000000000000000000000099");
692 let mock = server
693 .mock("POST", "/packed-account")
694 .with_status(400)
695 .with_header("content-type", "application/json")
696 .with_body(serde_json::json!({ "code": "account_does_not_exist", "message": "There is no account for this authenticator address" }).to_string())
697 .create_async()
698 .await;
699 let config = Config::new(
700 None,
701 1,
702 address!("0x0000000000000000000000000000000000000001"),
703 ServiceEndpoint::direct(indexer_url),
704 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
705 Vec::new(),
706 2,
707 )
708 .unwrap();
709
710 let indexer_client = ServiceClient::new(
711 reqwest::Client::new(),
712 ServiceKind::Indexer,
713 config.indexer(),
714 )
715 .unwrap();
716
717 let result = Authenticator::fetch_packed_account_data_for(
718 test_address,
719 None,
720 &config,
721 &indexer_client,
722 )
723 .await;
724
725 assert!(matches!(
726 result,
727 Err(AuthenticatorError::AccountDoesNotExist)
728 ));
729 mock.assert_async().await;
730 drop(server);
731 }
732
733 #[tokio::test]
734 #[cfg(not(target_arch = "wasm32"))]
735 async fn test_signing_nonce_from_indexer() {
736 let mut server = mockito::Server::new_async().await;
737 let indexer_url = server.url();
738 let leaf_index = U256::from(1);
739 let expected_nonce = U256::from(5);
740 let mock = server
741 .mock("POST", "/signature-nonce")
742 .match_header("content-type", "application/json")
743 .match_body(mockito::Matcher::JsonString(
744 serde_json::json!({ "leaf_index": format!("{:#x}", leaf_index) }).to_string(),
745 ))
746 .with_status(200)
747 .with_header("content-type", "application/json")
748 .with_body(
749 serde_json::json!({ "signature_nonce": format!("{:#x}", expected_nonce) })
750 .to_string(),
751 )
752 .create_async()
753 .await;
754 let config = Config::new(
755 None,
756 1,
757 address!("0x0000000000000000000000000000000000000001"),
758 ServiceEndpoint::direct(indexer_url),
759 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
760 Vec::new(),
761 2,
762 )
763 .unwrap();
764
765 let http_client = reqwest::Client::new();
766 let authenticator = Authenticator {
767 config: config.clone(),
768 packed_account_data: leaf_index,
769 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
770 registry: None,
771 indexer_client: ServiceClient::new(
772 http_client.clone(),
773 ServiceKind::Indexer,
774 config.indexer(),
775 )
776 .unwrap(),
777 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
778 .unwrap(),
779 ws_connector: Connector::Plain,
780 query_material: None,
781 nullifier_material: None,
782 };
783 let nonce = authenticator.signing_nonce().await.unwrap();
784 assert_eq!(nonce, expected_nonce);
785 mock.assert_async().await;
786 drop(server);
787 }
788
789 #[test]
790 fn test_danger_sign_challenge_returns_valid_signature() {
791 let config = Config::new(
792 None,
793 1,
794 address!("0x0000000000000000000000000000000000000001"),
795 ServiceEndpoint::direct("http://indexer.example.com".to_string()),
796 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
797 Vec::new(),
798 2,
799 )
800 .unwrap();
801 let http_client = reqwest::Client::new();
802 let authenticator = Authenticator {
803 indexer_client: ServiceClient::new(
804 http_client.clone(),
805 ServiceKind::Indexer,
806 config.indexer(),
807 )
808 .unwrap(),
809 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
810 .unwrap(),
811 config,
812 packed_account_data: U256::from(1),
813 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
814 registry: None,
815 ws_connector: Connector::Plain,
816 query_material: None,
817 nullifier_material: None,
818 };
819 let challenge = b"test challenge";
820 let signature = authenticator.danger_sign_challenge(challenge).unwrap();
821 let recovered = signature
822 .recover_address_from_msg(challenge)
823 .expect("should recover address");
824 assert_eq!(recovered, authenticator.onchain_address());
825 }
826
827 #[test]
828 fn test_danger_sign_challenge_different_challenges_different_signatures() {
829 let config = Config::new(
830 None,
831 1,
832 address!("0x0000000000000000000000000000000000000001"),
833 ServiceEndpoint::direct("http://indexer.example.com".to_string()),
834 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
835 Vec::new(),
836 2,
837 )
838 .unwrap();
839 let http_client = reqwest::Client::new();
840 let authenticator = Authenticator {
841 indexer_client: ServiceClient::new(
842 http_client.clone(),
843 ServiceKind::Indexer,
844 config.indexer(),
845 )
846 .unwrap(),
847 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
848 .unwrap(),
849 config,
850 packed_account_data: U256::from(1),
851 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
852 registry: None,
853 ws_connector: Connector::Plain,
854 query_material: None,
855 nullifier_material: None,
856 };
857 let sig_a = authenticator.danger_sign_challenge(b"challenge A").unwrap();
858 let sig_b = authenticator.danger_sign_challenge(b"challenge B").unwrap();
859 assert_ne!(sig_a, sig_b);
860 }
861
862 #[test]
863 fn test_danger_sign_challenge_deterministic() {
864 let config = Config::new(
865 None,
866 1,
867 address!("0x0000000000000000000000000000000000000001"),
868 ServiceEndpoint::direct("http://indexer.example.com".to_string()),
869 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
870 Vec::new(),
871 2,
872 )
873 .unwrap();
874 let http_client = reqwest::Client::new();
875 let authenticator = Authenticator {
876 indexer_client: ServiceClient::new(
877 http_client.clone(),
878 ServiceKind::Indexer,
879 config.indexer(),
880 )
881 .unwrap(),
882 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
883 .unwrap(),
884 config,
885 packed_account_data: U256::from(1),
886 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
887 registry: None,
888 ws_connector: Connector::Plain,
889 query_material: None,
890 nullifier_material: None,
891 };
892 let challenge = b"deterministic test";
893 let sig1 = authenticator.danger_sign_challenge(challenge).unwrap();
894 let sig2 = authenticator.danger_sign_challenge(challenge).unwrap();
895 assert_eq!(sig1, sig2);
896 }
897
898 #[tokio::test]
899 #[cfg(not(target_arch = "wasm32"))]
900 async fn test_signing_nonce_from_indexer_error() {
901 let mut server = mockito::Server::new_async().await;
902 let indexer_url = server.url();
903 let mock = server
904 .mock("POST", "/signature-nonce")
905 .with_status(400)
906 .with_header("content-type", "application/json")
907 .with_body(serde_json::json!({ "code": "invalid_leaf_index", "message": "Account index cannot be zero" }).to_string())
908 .create_async()
909 .await;
910 let config = Config::new(
911 None,
912 1,
913 address!("0x0000000000000000000000000000000000000001"),
914 ServiceEndpoint::direct(indexer_url),
915 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
916 Vec::new(),
917 2,
918 )
919 .unwrap();
920
921 let http_client = reqwest::Client::new();
922 let authenticator = Authenticator {
923 config: config.clone(),
924 packed_account_data: U256::ZERO,
925 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
926 registry: None,
927 indexer_client: ServiceClient::new(
928 http_client.clone(),
929 ServiceKind::Indexer,
930 config.indexer(),
931 )
932 .unwrap(),
933 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
934 .unwrap(),
935 ws_connector: Connector::Plain,
936 query_material: None,
937 nullifier_material: None,
938 };
939 let result = authenticator.signing_nonce().await;
940 assert!(matches!(
941 result,
942 Err(AuthenticatorError::IndexerError { .. })
943 ));
944 mock.assert_async().await;
945 drop(server);
946 }
947}