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 ruint::{aliases::U256, uint};
30use taceo_oprf::client::Connector;
31use world_id_primitives::{
32 AuthenticatorPublicKeySet, PrimitiveError, SparseAuthenticatorPubkeysError,
33};
34pub use world_id_primitives::{Config, ServiceEndpoint, TREE_DEPTH, authenticator::ProtocolSigner};
35use world_id_proof::artifacts::ZkArtifactSource;
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) zk_artifact_source: Arc<dyn ZkArtifactSource>,
94}
95
96impl std::fmt::Debug for Authenticator {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("Authenticator")
100 .field("config", &self.config)
101 .finish_non_exhaustive()
102 }
103}
104
105impl Authenticator {
106 pub async fn init(
133 seed: &[u8],
134 config: Config,
135 zk_artifact_source: Arc<dyn ZkArtifactSource>,
136 ) -> Result<Self, AuthenticatorError> {
137 let signer = Signer::from_seed_bytes(seed)?;
138
139 let registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>> =
140 config.rpc_url().map(|rpc_url| {
141 let provider = alloy::providers::ProviderBuilder::new()
142 .with_chain_id(config.chain_id())
143 .connect_http(rpc_url.clone());
144 Arc::new(world_id_registries::world_id::WorldIdRegistry::new(
145 *config.registry_address(),
146 alloy::providers::Provider::erased(provider),
147 ))
148 });
149
150 let http_client = reqwest::Client::new();
151
152 let indexer_client =
153 ServiceClient::new(http_client.clone(), ServiceKind::Indexer, config.indexer())?;
154
155 let gateway_client =
156 ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())?;
157
158 let packed_account_data = Self::fetch_packed_account_data_for(
159 signer.onchain_signer_address(),
160 registry.as_deref(),
161 &config,
162 &indexer_client,
163 )
164 .await?;
165
166 #[cfg(not(target_arch = "wasm32"))]
167 let ws_connector = {
168 let mut root_store = rustls::RootCertStore::empty();
169 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
170 let rustls_config = rustls::ClientConfig::builder()
171 .with_root_certificates(root_store)
172 .with_no_client_auth();
173 Connector::Rustls(Arc::new(rustls_config))
174 };
175
176 #[cfg(target_arch = "wasm32")]
177 let ws_connector = Connector;
178
179 Ok(Self {
180 packed_account_data,
181 signer,
182 config,
183 registry,
184 indexer_client,
185 gateway_client,
186 ws_connector,
187 zk_artifact_source,
188 })
189 }
190
191 pub async fn register(
199 seed: &[u8],
200 config: Config,
201 recovery_address: Option<Address>,
202 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
203 let gateway_client = ServiceClient::new(
204 reqwest::Client::new(),
205 ServiceKind::Gateway,
206 config.gateway(),
207 )?;
208 InitializingAuthenticator::new(seed, config, recovery_address, gateway_client).await
209 }
210
211 pub async fn init_or_register(
224 seed: &[u8],
225 config: Config,
226 recovery_address: Option<Address>,
227 zk_artifact_source: Arc<dyn ZkArtifactSource>,
228 ) -> Result<Self, AuthenticatorError> {
229 match Self::init(seed, config.clone(), Arc::clone(&zk_artifact_source)).await {
230 Ok(authenticator) => Ok(authenticator),
231 Err(AuthenticatorError::AccountDoesNotExist) => {
232 let gateway_client = ServiceClient::new(
233 reqwest::Client::new(),
234 ServiceKind::Gateway,
235 config.gateway(),
236 )?;
237 let initializing_authenticator = InitializingAuthenticator::new(
238 seed,
239 config.clone(),
240 recovery_address,
241 gateway_client,
242 )
243 .await?;
244
245 let backoff = backon::ExponentialBuilder::default()
246 .with_min_delay(std::time::Duration::from_millis(800))
247 .with_factor(1.5)
248 .without_max_times()
249 .with_total_delay(Some(std::time::Duration::from_secs(120)));
250
251 let poller = || async {
252 let poll_status = initializing_authenticator.poll_status().await;
253 let result = match poll_status {
254 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
255 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
256 PollResult::TerminalError(AuthenticatorError::RegistrationError {
257 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
258 error_message: error,
259 }),
260 ),
261 Err(AuthenticatorError::GatewayError { status, body }) => {
262 if status.is_client_error() {
263 Err(PollResult::TerminalError(
264 AuthenticatorError::GatewayError { status, body },
265 ))
266 } else {
267 Err(PollResult::Retryable)
268 }
269 }
270 _ => Err(PollResult::Retryable),
271 };
272
273 match result {
274 Ok(()) => {
275 match Self::init(seed, config.clone(), Arc::clone(&zk_artifact_source))
276 .await
277 {
278 Ok(auth) => Ok(auth),
279 Err(AuthenticatorError::AccountDoesNotExist) => {
280 Err(PollResult::Retryable)
281 }
282 Err(e) => Err(PollResult::TerminalError(e)),
283 }
284 }
285 Err(e) => Err(e),
286 }
287 };
288
289 let result = backon::Retryable::retry(poller, backoff)
290 .when(|e| matches!(e, PollResult::Retryable))
291 .await;
292
293 match result {
294 Ok(authenticator) => Ok(authenticator),
295 Err(PollResult::TerminalError(e)) => Err(e),
296 Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
297 }
298 }
299 Err(e) => Err(e),
300 }
301 }
302
303 pub async fn fetch_packed_account_data(&self) -> Result<U256, AuthenticatorError> {
309 Self::fetch_packed_account_data_for(
310 self.onchain_address(),
311 self.registry().as_deref(),
312 &self.config,
313 &self.indexer_client,
314 )
315 .await
316 }
317
318 pub async fn refresh_packed_account_data(&mut self) -> Result<U256, AuthenticatorError> {
323 let packed_account_data = self.fetch_packed_account_data().await?;
324 self.packed_account_data = packed_account_data;
325 Ok(packed_account_data)
326 }
327
328 async fn fetch_packed_account_data_for(
336 onchain_signer_address: Address,
337 registry: Option<&WorldIdRegistryInstance<DynProvider>>,
338 config: &Config,
339 indexer_client: &ServiceClient,
340 ) -> Result<U256, AuthenticatorError> {
341 let raw_index = if let Some(registry) = registry {
343 registry
345 .getPackedAccountData(onchain_signer_address)
346 .call()
347 .await?
348 } else {
349 let req = IndexerPackedAccountRequest {
350 authenticator_address: onchain_signer_address,
351 };
352 match indexer_client
353 .post_json::<_, IndexerPackedAccountResponse>(
354 config.indexer_url(),
355 "/packed-account",
356 &req,
357 )
358 .await
359 {
360 Ok(response) => response.packed_account_data,
361 Err(AuthenticatorError::IndexerError { status, body }) => {
362 if let Ok(error_resp) =
363 serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
364 {
365 return match error_resp.code {
366 IndexerErrorCode::AccountDoesNotExist => {
367 Err(AuthenticatorError::AccountDoesNotExist)
368 }
369 _ => Err(AuthenticatorError::IndexerError {
370 status,
371 body: error_resp.message,
372 }),
373 };
374 }
375
376 return Err(AuthenticatorError::IndexerError { status, body });
377 }
378 Err(other) => return Err(other),
379 }
380 };
381
382 if raw_index == U256::ZERO {
383 return Err(AuthenticatorError::AccountDoesNotExist);
384 }
385
386 Ok(raw_index)
387 }
388
389 #[must_use]
392 pub const fn onchain_address(&self) -> Address {
393 self.signer.onchain_signer_address()
394 }
395
396 #[must_use]
399 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
400 self.signer.offchain_signer_pubkey()
401 }
402
403 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
408 let pk = self.signer.offchain_signer_pubkey().pk;
409 let mut compressed_bytes = Vec::new();
410 pk.serialize_compressed(&mut compressed_bytes)
411 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
412 Ok(U256::from_le_slice(&compressed_bytes))
413 }
414
415 #[must_use]
417 pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
418 self.registry.clone()
419 }
420
421 #[must_use]
437 pub fn leaf_index(&self) -> u64 {
438 (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
439 }
440
441 #[must_use]
445 pub fn recovery_counter(&self) -> U256 {
446 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
447 recovery_counter >> 224
448 }
449
450 #[must_use]
454 pub fn pubkey_id(&self) -> U256 {
455 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
456 pubkey_id >> 192
457 }
458
459 pub async fn fetch_inclusion_proof(
465 &self,
466 ) -> Result<AccountInclusionProof<TREE_DEPTH>, AuthenticatorError> {
467 let req = IndexerQueryRequest {
468 leaf_index: self.leaf_index(),
469 };
470 let response: AccountInclusionProof<TREE_DEPTH> = self
471 .indexer_client
472 .post_json(self.config.indexer_url(), "/inclusion-proof", &req)
473 .await?;
474
475 Ok(response)
476 }
477
478 pub async fn fetch_authenticator_pubkeys(
487 &self,
488 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
489 let req = IndexerQueryRequest {
490 leaf_index: self.leaf_index(),
491 };
492 let response: IndexerAuthenticatorPubkeysResponse = self
493 .indexer_client
494 .post_json(self.config.indexer_url(), "/authenticator-pubkeys", &req)
495 .await?;
496 Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
497 }
498
499 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
504 let registry = self.registry();
505 if let Some(registry) = registry {
506 let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
507 Ok(nonce)
508 } else {
509 let req = IndexerQueryRequest {
510 leaf_index: self.leaf_index(),
511 };
512 let response: IndexerSignatureNonceResponse = self
513 .indexer_client
514 .post_json(self.config.indexer_url(), "/signature-nonce", &req)
515 .await?;
516 Ok(response.signature_nonce)
517 }
518 }
519
520 pub fn danger_sign_challenge(&self, challenge: &[u8]) -> Result<Signature, AuthenticatorError> {
532 self.signer
533 .onchain_signer()
534 .sign_message_sync(challenge)
535 .map_err(|e| AuthenticatorError::Generic(format!("signature error: {e}")))
536 }
537
538 pub(crate) fn decode_indexer_pubkeys(
539 pubkeys: Vec<Option<U256>>,
540 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
541 AuthenticatorPublicKeySet::from_sparse_encoded_pubkeys(pubkeys).map_err(|e| match e {
542 SparseAuthenticatorPubkeysError::SlotOutOfBounds {
543 slot_index,
544 max_supported_slot,
545 } => AuthenticatorError::InvalidIndexerPubkeySlot {
546 slot_index,
547 max_supported_slot,
548 },
549 SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
550 PrimitiveError::Deserialization(format!(
551 "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
552 ))
553 .into()
554 }
555 })
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 use crate::{error::AuthenticatorError, traits::OnchainKeyRepresentable};
563 use alloy::primitives::{U256, address};
564 use world_id_primitives::MAX_AUTHENTICATOR_KEYS;
565
566 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
567 Signer::from_seed_bytes(&[seed_byte; 32])
568 .unwrap()
569 .offchain_signer_pubkey()
570 }
571
572 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
573 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
574 }
575
576 fn dummy_zk_artifact_source() -> Arc<dyn ZkArtifactSource> {
577 Arc::new(world_id_proof::artifacts::dummy::DummyZkArtifactSource)
578 }
579
580 #[test]
581 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
582 let mut key_set =
583 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
584 .unwrap();
585 key_set[1] = None;
586 let new_key = test_pubkey(3);
587
588 let index = key_set.insert_or_reuse(new_key).unwrap();
589
590 assert_eq!(index, 1);
591 assert_eq!(key_set.len(), 3);
592 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
593 }
594
595 #[test]
596 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
597 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
598 let new_key = test_pubkey(2);
599
600 let index = key_set.insert_or_reuse(new_key).unwrap();
601
602 assert_eq!(index, 1);
603 assert_eq!(key_set.len(), 2);
604 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
605 }
606
607 #[test]
608 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
609 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
610 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
611
612 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
613
614 assert_eq!(key_set.len(), 2);
615 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
616 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
617 }
618
619 #[test]
620 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
621 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
622 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
623
624 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
625 assert!(matches!(
626 error,
627 AuthenticatorError::InvalidIndexerPubkeySlot {
628 slot_index,
629 max_supported_slot
630 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
631 ));
632 }
633
634 #[tokio::test]
635 async fn test_get_packed_account_data_from_indexer() {
636 let mut server = mockito::Server::new_async().await;
637 let indexer_url = server.url();
638 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
639 let expected_packed_index = U256::from(42);
640 let mock = server
641 .mock("POST", "/packed-account")
642 .match_header("content-type", "application/json")
643 .match_body(mockito::Matcher::JsonString(
644 serde_json::json!({ "authenticator_address": test_address }).to_string(),
645 ))
646 .with_status(200)
647 .with_header("content-type", "application/json")
648 .with_body(
649 serde_json::json!({ "packed_account_data": format!("{:#x}", expected_packed_index) }).to_string(),
650 )
651 .create_async()
652 .await;
653 let config = Config::new(
654 None,
655 1,
656 address!("0x0000000000000000000000000000000000000001"),
657 ServiceEndpoint::direct(indexer_url),
658 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
659 Vec::new(),
660 2,
661 )
662 .unwrap();
663
664 let indexer_client = ServiceClient::new(
665 reqwest::Client::new(),
666 ServiceKind::Indexer,
667 config.indexer(),
668 )
669 .unwrap();
670
671 let result = Authenticator::fetch_packed_account_data_for(
672 test_address,
673 None, &config,
675 &indexer_client,
676 )
677 .await
678 .unwrap();
679
680 assert_eq!(result, expected_packed_index);
681 mock.assert_async().await;
682 drop(server);
683 }
684
685 #[tokio::test]
686 async fn test_get_packed_account_data_from_indexer_error() {
687 let mut server = mockito::Server::new_async().await;
688 let indexer_url = server.url();
689 let test_address = address!("0x0000000000000000000000000000000000000099");
690 let mock = server
691 .mock("POST", "/packed-account")
692 .with_status(400)
693 .with_header("content-type", "application/json")
694 .with_body(serde_json::json!({ "code": "account_does_not_exist", "message": "There is no account for this authenticator address" }).to_string())
695 .create_async()
696 .await;
697 let config = Config::new(
698 None,
699 1,
700 address!("0x0000000000000000000000000000000000000001"),
701 ServiceEndpoint::direct(indexer_url),
702 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
703 Vec::new(),
704 2,
705 )
706 .unwrap();
707
708 let indexer_client = ServiceClient::new(
709 reqwest::Client::new(),
710 ServiceKind::Indexer,
711 config.indexer(),
712 )
713 .unwrap();
714
715 let result = Authenticator::fetch_packed_account_data_for(
716 test_address,
717 None,
718 &config,
719 &indexer_client,
720 )
721 .await;
722
723 assert!(matches!(
724 result,
725 Err(AuthenticatorError::AccountDoesNotExist)
726 ));
727 mock.assert_async().await;
728 drop(server);
729 }
730
731 #[tokio::test]
732 #[cfg(not(target_arch = "wasm32"))]
733 async fn test_signing_nonce_from_indexer() {
734 let mut server = mockito::Server::new_async().await;
735 let indexer_url = server.url();
736 let leaf_index = U256::from(1);
737 let expected_nonce = U256::from(5);
738 let mock = server
739 .mock("POST", "/signature-nonce")
740 .match_header("content-type", "application/json")
741 .match_body(mockito::Matcher::JsonString(
742 serde_json::json!({ "leaf_index": format!("{:#x}", leaf_index) }).to_string(),
743 ))
744 .with_status(200)
745 .with_header("content-type", "application/json")
746 .with_body(
747 serde_json::json!({ "signature_nonce": format!("{:#x}", expected_nonce) })
748 .to_string(),
749 )
750 .create_async()
751 .await;
752 let config = Config::new(
753 None,
754 1,
755 address!("0x0000000000000000000000000000000000000001"),
756 ServiceEndpoint::direct(indexer_url),
757 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
758 Vec::new(),
759 2,
760 )
761 .unwrap();
762
763 let http_client = reqwest::Client::new();
764 let authenticator = Authenticator {
765 config: config.clone(),
766 packed_account_data: leaf_index,
767 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
768 registry: None,
769 indexer_client: ServiceClient::new(
770 http_client.clone(),
771 ServiceKind::Indexer,
772 config.indexer(),
773 )
774 .unwrap(),
775 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
776 .unwrap(),
777 ws_connector: Connector::Plain,
778 zk_artifact_source: dummy_zk_artifact_source(),
779 };
780 let nonce = authenticator.signing_nonce().await.unwrap();
781 assert_eq!(nonce, expected_nonce);
782 mock.assert_async().await;
783 drop(server);
784 }
785
786 #[test]
787 fn test_danger_sign_challenge_returns_valid_signature() {
788 let config = Config::new(
789 None,
790 1,
791 address!("0x0000000000000000000000000000000000000001"),
792 ServiceEndpoint::direct("http://indexer.example.com".to_string()),
793 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
794 Vec::new(),
795 2,
796 )
797 .unwrap();
798 let http_client = reqwest::Client::new();
799 let authenticator = Authenticator {
800 indexer_client: ServiceClient::new(
801 http_client.clone(),
802 ServiceKind::Indexer,
803 config.indexer(),
804 )
805 .unwrap(),
806 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
807 .unwrap(),
808 config,
809 packed_account_data: U256::from(1),
810 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
811 registry: None,
812 ws_connector: Connector::Plain,
813 zk_artifact_source: dummy_zk_artifact_source(),
814 };
815 let challenge = b"test challenge";
816 let signature = authenticator.danger_sign_challenge(challenge).unwrap();
817 let recovered = signature
818 .recover_address_from_msg(challenge)
819 .expect("should recover address");
820 assert_eq!(recovered, authenticator.onchain_address());
821 }
822
823 #[test]
824 fn test_danger_sign_challenge_different_challenges_different_signatures() {
825 let config = Config::new(
826 None,
827 1,
828 address!("0x0000000000000000000000000000000000000001"),
829 ServiceEndpoint::direct("http://indexer.example.com".to_string()),
830 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
831 Vec::new(),
832 2,
833 )
834 .unwrap();
835 let http_client = reqwest::Client::new();
836 let authenticator = Authenticator {
837 indexer_client: ServiceClient::new(
838 http_client.clone(),
839 ServiceKind::Indexer,
840 config.indexer(),
841 )
842 .unwrap(),
843 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
844 .unwrap(),
845 config,
846 packed_account_data: U256::from(1),
847 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
848 registry: None,
849 ws_connector: Connector::Plain,
850 zk_artifact_source: dummy_zk_artifact_source(),
851 };
852 let sig_a = authenticator.danger_sign_challenge(b"challenge A").unwrap();
853 let sig_b = authenticator.danger_sign_challenge(b"challenge B").unwrap();
854 assert_ne!(sig_a, sig_b);
855 }
856
857 #[test]
858 fn test_danger_sign_challenge_deterministic() {
859 let config = Config::new(
860 None,
861 1,
862 address!("0x0000000000000000000000000000000000000001"),
863 ServiceEndpoint::direct("http://indexer.example.com".to_string()),
864 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
865 Vec::new(),
866 2,
867 )
868 .unwrap();
869 let http_client = reqwest::Client::new();
870 let authenticator = Authenticator {
871 indexer_client: ServiceClient::new(
872 http_client.clone(),
873 ServiceKind::Indexer,
874 config.indexer(),
875 )
876 .unwrap(),
877 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
878 .unwrap(),
879 config,
880 packed_account_data: U256::from(1),
881 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
882 registry: None,
883 ws_connector: Connector::Plain,
884 zk_artifact_source: dummy_zk_artifact_source(),
885 };
886 let challenge = b"deterministic test";
887 let sig1 = authenticator.danger_sign_challenge(challenge).unwrap();
888 let sig2 = authenticator.danger_sign_challenge(challenge).unwrap();
889 assert_eq!(sig1, sig2);
890 }
891
892 #[tokio::test]
893 #[cfg(not(target_arch = "wasm32"))]
894 async fn test_signing_nonce_from_indexer_error() {
895 let mut server = mockito::Server::new_async().await;
896 let indexer_url = server.url();
897 let mock = server
898 .mock("POST", "/signature-nonce")
899 .with_status(400)
900 .with_header("content-type", "application/json")
901 .with_body(serde_json::json!({ "code": "invalid_leaf_index", "message": "Account index cannot be zero" }).to_string())
902 .create_async()
903 .await;
904 let config = Config::new(
905 None,
906 1,
907 address!("0x0000000000000000000000000000000000000001"),
908 ServiceEndpoint::direct(indexer_url),
909 ServiceEndpoint::direct("http://gateway.example.com".to_string()),
910 Vec::new(),
911 2,
912 )
913 .unwrap();
914
915 let http_client = reqwest::Client::new();
916 let authenticator = Authenticator {
917 config: config.clone(),
918 packed_account_data: U256::ZERO,
919 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
920 registry: None,
921 indexer_client: ServiceClient::new(
922 http_client.clone(),
923 ServiceKind::Indexer,
924 config.indexer(),
925 )
926 .unwrap(),
927 gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
928 .unwrap(),
929 ws_connector: Connector::Plain,
930 zk_artifact_source: dummy_zk_artifact_source(),
931 };
932 let result = authenticator.signing_nonce().await;
933 assert!(matches!(
934 result,
935 Err(AuthenticatorError::IndexerError { .. })
936 ));
937 mock.assert_async().await;
938 drop(server);
939 }
940}