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 serde::{Deserialize, Serialize};
20use world_id_primitives::{Credential, FieldElement, ProofResponse, Signer};
21
22pub use crate::ohttp::OhttpClientConfig;
23use crate::registry::WorldIdRegistry::WorldIdRegistryInstance;
24use alloy::{
25 primitives::Address,
26 providers::DynProvider,
27 signers::{Signature, SignerSync},
28};
29use ark_serialize::CanonicalSerialize;
30use eddsa_babyjubjub::EdDSAPublicKey;
31use groth16_material::circom::CircomGroth16Material;
32use ruint::{aliases::U256, uint};
33use taceo_oprf::client::Connector;
34pub use world_id_primitives::{Config, TREE_DEPTH, authenticator::ProtocolSigner};
35use world_id_primitives::{
36 PrimitiveError,
37 authenticator::{
38 AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError,
39 decode_sparse_authenticator_pubkeys,
40 },
41};
42
43#[expect(unused_imports, reason = "used for docs")]
44use world_id_primitives::{Nullifier, SessionId};
45
46static MASK_RECOVERY_COUNTER: U256 =
47 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
48static MASK_PUBKEY_ID: U256 =
49 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
50static MASK_LEAF_INDEX: U256 =
51 uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
52
53#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct AuthenticatorConfig {
57 #[serde(flatten)]
59 pub config: Config,
60 #[serde(default)]
62 pub ohttp_indexer: Option<OhttpClientConfig>,
63 #[serde(default)]
65 pub ohttp_gateway: Option<OhttpClientConfig>,
66}
67
68impl AuthenticatorConfig {
69 pub fn from_json(json_str: &str) -> Result<Self, AuthenticatorError> {
77 serde_json::from_str(json_str).map_err(|e| {
78 AuthenticatorError::from(PrimitiveError::Serialization(format!(
79 "failed to parse authenticator config: {e}"
80 )))
81 })
82 }
83}
84
85impl From<Config> for AuthenticatorConfig {
86 fn from(config: Config) -> Self {
87 Self {
88 config,
89 ohttp_indexer: None,
90 ohttp_gateway: None,
91 }
92 }
93}
94
95pub struct CredentialInput {
97 pub credential: Credential,
99 pub blinding_factor: FieldElement,
101}
102
103#[derive(Debug)]
108pub struct ProofResult {
109 pub session_id_r_seed: Option<FieldElement>,
113
114 pub proof_response: ProofResponse,
116}
117
118pub struct Authenticator {
130 pub config: Config,
132 pub packed_account_data: U256,
135 pub(crate) signer: Signer,
136 pub(crate) registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
137 pub(crate) indexer_client: ServiceClient,
138 pub(crate) gateway_client: ServiceClient,
139 pub(crate) ws_connector: Connector,
140 pub(crate) query_material: Option<Arc<CircomGroth16Material>>,
141 pub(crate) nullifier_material: Option<Arc<CircomGroth16Material>>,
142}
143
144impl std::fmt::Debug for Authenticator {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.debug_struct("Authenticator")
148 .field("config", &self.config)
149 .finish_non_exhaustive()
150 }
151}
152
153impl Authenticator {
154 pub async fn init(
164 seed: &[u8],
165 config: AuthenticatorConfig,
166 ) -> Result<Self, AuthenticatorError> {
167 let AuthenticatorConfig {
168 config,
169 ohttp_indexer,
170 ohttp_gateway,
171 } = config;
172
173 let signer = Signer::from_seed_bytes(seed)?;
174
175 let registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>> =
176 config.rpc_url().map(|rpc_url| {
177 let provider = alloy::providers::ProviderBuilder::new()
178 .with_chain_id(config.chain_id())
179 .connect_http(rpc_url.clone());
180 Arc::new(crate::registry::WorldIdRegistry::new(
181 *config.registry_address(),
182 alloy::providers::Provider::erased(provider),
183 ))
184 });
185
186 let http_client = reqwest::Client::new();
187
188 let indexer_client = ServiceClient::new(
189 http_client.clone(),
190 ServiceKind::Indexer,
191 config.indexer_url(),
192 ohttp_indexer,
193 )?;
194
195 let gateway_client = ServiceClient::new(
196 http_client,
197 ServiceKind::Gateway,
198 config.gateway_url(),
199 ohttp_gateway,
200 )?;
201
202 let packed_account_data = Self::get_packed_account_data(
203 signer.onchain_signer_address(),
204 registry.as_deref(),
205 &config,
206 &indexer_client,
207 )
208 .await?;
209
210 #[cfg(not(target_arch = "wasm32"))]
211 let ws_connector = {
212 let mut root_store = rustls::RootCertStore::empty();
213 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
214 let rustls_config = rustls::ClientConfig::builder()
215 .with_root_certificates(root_store)
216 .with_no_client_auth();
217 Connector::Rustls(Arc::new(rustls_config))
218 };
219
220 #[cfg(target_arch = "wasm32")]
221 let ws_connector = Connector;
222
223 Ok(Self {
224 packed_account_data,
225 signer,
226 config,
227 registry,
228 indexer_client,
229 gateway_client,
230 ws_connector,
231 query_material: None,
232 nullifier_material: None,
233 })
234 }
235
236 #[must_use]
241 pub fn with_proof_materials(
242 self,
243 query_material: Arc<CircomGroth16Material>,
244 nullifier_material: Arc<CircomGroth16Material>,
245 ) -> Self {
246 Self {
247 query_material: Some(query_material),
248 nullifier_material: Some(nullifier_material),
249 ..self
250 }
251 }
252
253 pub async fn register(
261 seed: &[u8],
262 config: AuthenticatorConfig,
263 recovery_address: Option<Address>,
264 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
265 let AuthenticatorConfig {
266 config,
267 ohttp_gateway,
268 ..
269 } = config;
270 let gateway_client = ServiceClient::new(
271 reqwest::Client::new(),
272 ServiceKind::Gateway,
273 config.gateway_url(),
274 ohttp_gateway,
275 )?;
276 InitializingAuthenticator::new(seed, config, recovery_address, gateway_client).await
277 }
278
279 pub async fn init_or_register(
292 seed: &[u8],
293 config: AuthenticatorConfig,
294 recovery_address: Option<Address>,
295 ) -> Result<Self, AuthenticatorError> {
296 match Self::init(seed, config.clone()).await {
297 Ok(authenticator) => Ok(authenticator),
298 Err(AuthenticatorError::AccountDoesNotExist) => {
299 let gateway_client = ServiceClient::new(
300 reqwest::Client::new(),
301 ServiceKind::Gateway,
302 config.config.gateway_url(),
303 config.ohttp_gateway.clone(),
304 )?;
305 let initializing_authenticator = InitializingAuthenticator::new(
306 seed,
307 config.config.clone(),
308 recovery_address,
309 gateway_client,
310 )
311 .await?;
312
313 let backoff = backon::ExponentialBuilder::default()
314 .with_min_delay(std::time::Duration::from_millis(800))
315 .with_factor(1.5)
316 .without_max_times()
317 .with_total_delay(Some(std::time::Duration::from_secs(120)));
318
319 let poller = || async {
320 let poll_status = initializing_authenticator.poll_status().await;
321 let result = match poll_status {
322 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
323 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
324 PollResult::TerminalError(AuthenticatorError::RegistrationError {
325 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
326 error_message: error,
327 }),
328 ),
329 Err(AuthenticatorError::GatewayError { status, body }) => {
330 if status.is_client_error() {
331 Err(PollResult::TerminalError(
332 AuthenticatorError::GatewayError { status, body },
333 ))
334 } else {
335 Err(PollResult::Retryable)
336 }
337 }
338 _ => Err(PollResult::Retryable),
339 };
340
341 match result {
342 Ok(()) => match Self::init(seed, config.clone()).await {
343 Ok(auth) => Ok(auth),
344 Err(AuthenticatorError::AccountDoesNotExist) => {
345 Err(PollResult::Retryable)
346 }
347 Err(e) => Err(PollResult::TerminalError(e)),
348 },
349 Err(e) => Err(e),
350 }
351 };
352
353 let result = backon::Retryable::retry(poller, backoff)
354 .when(|e| matches!(e, PollResult::Retryable))
355 .await;
356
357 match result {
358 Ok(authenticator) => Ok(authenticator),
359 Err(PollResult::TerminalError(e)) => Err(e),
360 Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
361 }
362 }
363 Err(e) => Err(e),
364 }
365 }
366
367 pub async fn refresh_packed_account_data(&self) -> Result<U256, AuthenticatorError> {
372 Self::get_packed_account_data(
373 self.onchain_address(),
374 self.registry().as_deref(),
375 &self.config,
376 &self.indexer_client,
377 )
378 .await
379 }
380
381 async fn get_packed_account_data(
389 onchain_signer_address: Address,
390 registry: Option<&WorldIdRegistryInstance<DynProvider>>,
391 config: &Config,
392 indexer_client: &ServiceClient,
393 ) -> Result<U256, AuthenticatorError> {
394 let raw_index = if let Some(registry) = registry {
396 registry
398 .getPackedAccountData(onchain_signer_address)
399 .call()
400 .await?
401 } else {
402 let req = IndexerPackedAccountRequest {
403 authenticator_address: onchain_signer_address,
404 };
405 match indexer_client
406 .post_json::<_, IndexerPackedAccountResponse>(
407 config.indexer_url(),
408 "/packed-account",
409 &req,
410 )
411 .await
412 {
413 Ok(response) => response.packed_account_data,
414 Err(AuthenticatorError::IndexerError { status, body }) => {
415 if let Ok(error_resp) =
416 serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
417 {
418 return match error_resp.code {
419 IndexerErrorCode::AccountDoesNotExist => {
420 Err(AuthenticatorError::AccountDoesNotExist)
421 }
422 _ => Err(AuthenticatorError::IndexerError {
423 status,
424 body: error_resp.message,
425 }),
426 };
427 }
428
429 return Err(AuthenticatorError::IndexerError { status, body });
430 }
431 Err(other) => return Err(other),
432 }
433 };
434
435 if raw_index == U256::ZERO {
436 return Err(AuthenticatorError::AccountDoesNotExist);
437 }
438
439 Ok(raw_index)
440 }
441
442 #[must_use]
445 pub const fn onchain_address(&self) -> Address {
446 self.signer.onchain_signer_address()
447 }
448
449 #[must_use]
452 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
453 self.signer.offchain_signer_pubkey()
454 }
455
456 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
461 let pk = self.signer.offchain_signer_pubkey().pk;
462 let mut compressed_bytes = Vec::new();
463 pk.serialize_compressed(&mut compressed_bytes)
464 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
465 Ok(U256::from_le_slice(&compressed_bytes))
466 }
467
468 #[must_use]
470 pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
471 self.registry.clone()
472 }
473
474 #[must_use]
490 pub fn leaf_index(&self) -> u64 {
491 (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
492 }
493
494 #[must_use]
498 pub fn recovery_counter(&self) -> U256 {
499 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
500 recovery_counter >> 224
501 }
502
503 #[must_use]
507 pub fn pubkey_id(&self) -> U256 {
508 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
509 pubkey_id >> 192
510 }
511
512 pub async fn fetch_inclusion_proof(
518 &self,
519 ) -> Result<AccountInclusionProof<TREE_DEPTH>, AuthenticatorError> {
520 let req = IndexerQueryRequest {
521 leaf_index: self.leaf_index(),
522 };
523 let response: AccountInclusionProof<TREE_DEPTH> = self
524 .indexer_client
525 .post_json(self.config.indexer_url(), "/inclusion-proof", &req)
526 .await?;
527
528 Ok(response)
529 }
530
531 pub async fn fetch_authenticator_pubkeys(
540 &self,
541 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
542 let req = IndexerQueryRequest {
543 leaf_index: self.leaf_index(),
544 };
545 let response: IndexerAuthenticatorPubkeysResponse = self
546 .indexer_client
547 .post_json(self.config.indexer_url(), "/authenticator-pubkeys", &req)
548 .await?;
549 Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
550 }
551
552 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
557 let registry = self.registry();
558 if let Some(registry) = registry {
559 let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
560 Ok(nonce)
561 } else {
562 let req = IndexerQueryRequest {
563 leaf_index: self.leaf_index(),
564 };
565 let response: IndexerSignatureNonceResponse = self
566 .indexer_client
567 .post_json(self.config.indexer_url(), "/signature-nonce", &req)
568 .await?;
569 Ok(response.signature_nonce)
570 }
571 }
572
573 pub fn danger_sign_challenge(&self, challenge: &[u8]) -> Result<Signature, AuthenticatorError> {
585 self.signer
586 .onchain_signer()
587 .sign_message_sync(challenge)
588 .map_err(|e| AuthenticatorError::Generic(format!("signature error: {e}")))
589 }
590
591 pub(crate) fn decode_indexer_pubkeys(
592 pubkeys: Vec<Option<U256>>,
593 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
594 decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
595 SparseAuthenticatorPubkeysError::SlotOutOfBounds {
596 slot_index,
597 max_supported_slot,
598 } => AuthenticatorError::InvalidIndexerPubkeySlot {
599 slot_index,
600 max_supported_slot,
601 },
602 SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
603 PrimitiveError::Deserialization(format!(
604 "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
605 ))
606 .into()
607 }
608 })
609 }
610
611 pub(crate) fn insert_or_reuse_authenticator_key(
612 key_set: &mut AuthenticatorPublicKeySet,
613 new_authenticator_pubkey: EdDSAPublicKey,
614 ) -> Result<usize, AuthenticatorError> {
615 if let Some(index) = key_set.iter().position(Option::is_none) {
616 key_set.try_set_at_index(index, new_authenticator_pubkey)?;
617 Ok(index)
618 } else {
619 key_set.try_push(new_authenticator_pubkey)?;
620 Ok(key_set.len() - 1)
621 }
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use crate::{error::AuthenticatorError, traits::OnchainKeyRepresentable};
629 use alloy::primitives::{U256, address};
630 use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
631
632 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
633 Signer::from_seed_bytes(&[seed_byte; 32])
634 .unwrap()
635 .offchain_signer_pubkey()
636 }
637
638 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
639 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
640 }
641
642 #[test]
643 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
644 let mut key_set =
645 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
646 .unwrap();
647 key_set[1] = None;
648 let new_key = test_pubkey(3);
649
650 let index =
651 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
652
653 assert_eq!(index, 1);
654 assert_eq!(key_set.len(), 3);
655 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
656 }
657
658 #[test]
659 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
660 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
661 let new_key = test_pubkey(2);
662
663 let index =
664 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
665
666 assert_eq!(index, 1);
667 assert_eq!(key_set.len(), 2);
668 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
669 }
670
671 #[test]
672 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
673 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
674 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
675
676 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
677
678 assert_eq!(key_set.len(), 2);
679 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
680 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
681 }
682
683 #[test]
684 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
685 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
686 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
687
688 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
689 assert!(matches!(
690 error,
691 AuthenticatorError::InvalidIndexerPubkeySlot {
692 slot_index,
693 max_supported_slot
694 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
695 ));
696 }
697
698 #[tokio::test]
699 async fn test_get_packed_account_data_from_indexer() {
700 let mut server = mockito::Server::new_async().await;
701 let indexer_url = server.url();
702 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
703 let expected_packed_index = U256::from(42);
704 let mock = server
705 .mock("POST", "/packed-account")
706 .match_header("content-type", "application/json")
707 .match_body(mockito::Matcher::JsonString(
708 serde_json::json!({ "authenticator_address": test_address }).to_string(),
709 ))
710 .with_status(200)
711 .with_header("content-type", "application/json")
712 .with_body(
713 serde_json::json!({ "packed_account_data": format!("{:#x}", expected_packed_index) }).to_string(),
714 )
715 .create_async()
716 .await;
717 let config = Config::new(
718 None,
719 1,
720 address!("0x0000000000000000000000000000000000000001"),
721 indexer_url,
722 "http://gateway.example.com".to_string(),
723 Vec::new(),
724 2,
725 )
726 .unwrap();
727
728 let indexer_client = ServiceClient::new(
729 reqwest::Client::new(),
730 ServiceKind::Indexer,
731 config.indexer_url(),
732 None,
733 )
734 .unwrap();
735
736 let result = Authenticator::get_packed_account_data(
737 test_address,
738 None, &config,
740 &indexer_client,
741 )
742 .await
743 .unwrap();
744
745 assert_eq!(result, expected_packed_index);
746 mock.assert_async().await;
747 drop(server);
748 }
749
750 #[tokio::test]
751 async fn test_get_packed_account_data_from_indexer_error() {
752 let mut server = mockito::Server::new_async().await;
753 let indexer_url = server.url();
754 let test_address = address!("0x0000000000000000000000000000000000000099");
755 let mock = server
756 .mock("POST", "/packed-account")
757 .with_status(400)
758 .with_header("content-type", "application/json")
759 .with_body(serde_json::json!({ "code": "account_does_not_exist", "message": "There is no account for this authenticator address" }).to_string())
760 .create_async()
761 .await;
762 let config = Config::new(
763 None,
764 1,
765 address!("0x0000000000000000000000000000000000000001"),
766 indexer_url,
767 "http://gateway.example.com".to_string(),
768 Vec::new(),
769 2,
770 )
771 .unwrap();
772
773 let indexer_client = ServiceClient::new(
774 reqwest::Client::new(),
775 ServiceKind::Indexer,
776 config.indexer_url(),
777 None,
778 )
779 .unwrap();
780
781 let result =
782 Authenticator::get_packed_account_data(test_address, None, &config, &indexer_client)
783 .await;
784
785 assert!(matches!(
786 result,
787 Err(AuthenticatorError::AccountDoesNotExist)
788 ));
789 mock.assert_async().await;
790 drop(server);
791 }
792
793 #[tokio::test]
794 #[cfg(not(target_arch = "wasm32"))]
795 async fn test_signing_nonce_from_indexer() {
796 let mut server = mockito::Server::new_async().await;
797 let indexer_url = server.url();
798 let leaf_index = U256::from(1);
799 let expected_nonce = U256::from(5);
800 let mock = server
801 .mock("POST", "/signature-nonce")
802 .match_header("content-type", "application/json")
803 .match_body(mockito::Matcher::JsonString(
804 serde_json::json!({ "leaf_index": format!("{:#x}", leaf_index) }).to_string(),
805 ))
806 .with_status(200)
807 .with_header("content-type", "application/json")
808 .with_body(
809 serde_json::json!({ "signature_nonce": format!("{:#x}", expected_nonce) })
810 .to_string(),
811 )
812 .create_async()
813 .await;
814 let config = Config::new(
815 None,
816 1,
817 address!("0x0000000000000000000000000000000000000001"),
818 indexer_url,
819 "http://gateway.example.com".to_string(),
820 Vec::new(),
821 2,
822 )
823 .unwrap();
824
825 let http_client = reqwest::Client::new();
826 let authenticator = Authenticator {
827 config: config.clone(),
828 packed_account_data: leaf_index,
829 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
830 registry: None,
831 indexer_client: ServiceClient::new(
832 http_client.clone(),
833 ServiceKind::Indexer,
834 config.indexer_url(),
835 None,
836 )
837 .unwrap(),
838 gateway_client: ServiceClient::new(
839 http_client,
840 ServiceKind::Gateway,
841 config.gateway_url(),
842 None,
843 )
844 .unwrap(),
845 ws_connector: Connector::Plain,
846 query_material: None,
847 nullifier_material: None,
848 };
849 let nonce = authenticator.signing_nonce().await.unwrap();
850 assert_eq!(nonce, expected_nonce);
851 mock.assert_async().await;
852 drop(server);
853 }
854
855 #[test]
856 fn test_danger_sign_challenge_returns_valid_signature() {
857 let config = Config::new(
858 None,
859 1,
860 address!("0x0000000000000000000000000000000000000001"),
861 "http://indexer.example.com".to_string(),
862 "http://gateway.example.com".to_string(),
863 Vec::new(),
864 2,
865 )
866 .unwrap();
867 let http_client = reqwest::Client::new();
868 let authenticator = Authenticator {
869 indexer_client: ServiceClient::new(
870 http_client.clone(),
871 ServiceKind::Indexer,
872 config.indexer_url(),
873 None,
874 )
875 .unwrap(),
876 gateway_client: ServiceClient::new(
877 http_client,
878 ServiceKind::Gateway,
879 config.gateway_url(),
880 None,
881 )
882 .unwrap(),
883 config,
884 packed_account_data: U256::from(1),
885 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
886 registry: None,
887 ws_connector: Connector::Plain,
888 query_material: None,
889 nullifier_material: None,
890 };
891 let challenge = b"test challenge";
892 let signature = authenticator.danger_sign_challenge(challenge).unwrap();
893 let recovered = signature
894 .recover_address_from_msg(challenge)
895 .expect("should recover address");
896 assert_eq!(recovered, authenticator.onchain_address());
897 }
898
899 #[test]
900 fn test_danger_sign_challenge_different_challenges_different_signatures() {
901 let config = Config::new(
902 None,
903 1,
904 address!("0x0000000000000000000000000000000000000001"),
905 "http://indexer.example.com".to_string(),
906 "http://gateway.example.com".to_string(),
907 Vec::new(),
908 2,
909 )
910 .unwrap();
911 let http_client = reqwest::Client::new();
912 let authenticator = Authenticator {
913 indexer_client: ServiceClient::new(
914 http_client.clone(),
915 ServiceKind::Indexer,
916 config.indexer_url(),
917 None,
918 )
919 .unwrap(),
920 gateway_client: ServiceClient::new(
921 http_client,
922 ServiceKind::Gateway,
923 config.gateway_url(),
924 None,
925 )
926 .unwrap(),
927 config,
928 packed_account_data: U256::from(1),
929 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
930 registry: None,
931 ws_connector: Connector::Plain,
932 query_material: None,
933 nullifier_material: None,
934 };
935 let sig_a = authenticator.danger_sign_challenge(b"challenge A").unwrap();
936 let sig_b = authenticator.danger_sign_challenge(b"challenge B").unwrap();
937 assert_ne!(sig_a, sig_b);
938 }
939
940 #[test]
941 fn test_danger_sign_challenge_deterministic() {
942 let config = Config::new(
943 None,
944 1,
945 address!("0x0000000000000000000000000000000000000001"),
946 "http://indexer.example.com".to_string(),
947 "http://gateway.example.com".to_string(),
948 Vec::new(),
949 2,
950 )
951 .unwrap();
952 let http_client = reqwest::Client::new();
953 let authenticator = Authenticator {
954 indexer_client: ServiceClient::new(
955 http_client.clone(),
956 ServiceKind::Indexer,
957 config.indexer_url(),
958 None,
959 )
960 .unwrap(),
961 gateway_client: ServiceClient::new(
962 http_client,
963 ServiceKind::Gateway,
964 config.gateway_url(),
965 None,
966 )
967 .unwrap(),
968 config,
969 packed_account_data: U256::from(1),
970 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
971 registry: None,
972 ws_connector: Connector::Plain,
973 query_material: None,
974 nullifier_material: None,
975 };
976 let challenge = b"deterministic test";
977 let sig1 = authenticator.danger_sign_challenge(challenge).unwrap();
978 let sig2 = authenticator.danger_sign_challenge(challenge).unwrap();
979 assert_eq!(sig1, sig2);
980 }
981
982 #[tokio::test]
983 #[cfg(not(target_arch = "wasm32"))]
984 async fn test_signing_nonce_from_indexer_error() {
985 let mut server = mockito::Server::new_async().await;
986 let indexer_url = server.url();
987 let mock = server
988 .mock("POST", "/signature-nonce")
989 .with_status(400)
990 .with_header("content-type", "application/json")
991 .with_body(serde_json::json!({ "code": "invalid_leaf_index", "message": "Account index cannot be zero" }).to_string())
992 .create_async()
993 .await;
994 let config = Config::new(
995 None,
996 1,
997 address!("0x0000000000000000000000000000000000000001"),
998 indexer_url,
999 "http://gateway.example.com".to_string(),
1000 Vec::new(),
1001 2,
1002 )
1003 .unwrap();
1004
1005 let http_client = reqwest::Client::new();
1006 let authenticator = Authenticator {
1007 config: config.clone(),
1008 packed_account_data: U256::ZERO,
1009 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1010 registry: None,
1011 indexer_client: ServiceClient::new(
1012 http_client.clone(),
1013 ServiceKind::Indexer,
1014 config.indexer_url(),
1015 None,
1016 )
1017 .unwrap(),
1018 gateway_client: ServiceClient::new(
1019 http_client,
1020 ServiceKind::Gateway,
1021 config.gateway_url(),
1022 None,
1023 )
1024 .unwrap(),
1025 ws_connector: Connector::Plain,
1026 query_material: None,
1027 nullifier_material: None,
1028 };
1029 let result = authenticator.signing_nonce().await;
1030 assert!(matches!(
1031 result,
1032 Err(AuthenticatorError::IndexerError { .. })
1033 ));
1034 mock.assert_async().await;
1035 drop(server);
1036 }
1037
1038 #[test]
1039 fn test_authenticator_config_from_json_plain_config() {
1040 let json = serde_json::json!({
1041 "chain_id": 480,
1042 "registry_address": "0x0000000000000000000000000000000000000001",
1043 "indexer_url": "http://indexer.example.com",
1044 "gateway_url": "http://gateway.example.com",
1045 "nullifier_oracle_urls": [],
1046 "nullifier_oracle_threshold": 2
1047 });
1048
1049 let config = AuthenticatorConfig::from_json(&json.to_string()).unwrap();
1050 assert!(config.ohttp_indexer.is_none());
1051 assert!(config.ohttp_gateway.is_none());
1052 assert_eq!(config.config.gateway_url(), "http://gateway.example.com");
1053 }
1054
1055 #[test]
1056 fn test_authenticator_config_from_json_with_ohttp() {
1057 let json = serde_json::json!({
1058 "chain_id": 480,
1059 "registry_address": "0x0000000000000000000000000000000000000001",
1060 "indexer_url": "http://indexer.example.com",
1061 "gateway_url": "http://gateway.example.com",
1062 "nullifier_oracle_urls": [],
1063 "nullifier_oracle_threshold": 2,
1064 "ohttp_indexer": {
1065 "relay_url": "https://relay.example.com/gateway",
1066 "key_config_base64": "dGVzdC1rZXk="
1067 },
1068 "ohttp_gateway": {
1069 "relay_url": "https://relay.example.com/gateway",
1070 "key_config_base64": "dGVzdC1rZXk="
1071 }
1072 });
1073
1074 let config = AuthenticatorConfig::from_json(&json.to_string()).unwrap();
1075 let ohttp_indexer = config.ohttp_indexer.unwrap();
1076 assert_eq!(ohttp_indexer.relay_url, "https://relay.example.com/gateway");
1077 assert_eq!(ohttp_indexer.key_config_base64, "dGVzdC1rZXk=");
1078 let ohttp_gateway = config.ohttp_gateway.unwrap();
1079 assert_eq!(ohttp_gateway.relay_url, "https://relay.example.com/gateway");
1080 assert_eq!(ohttp_gateway.key_config_base64, "dGVzdC1rZXk=");
1081 }
1082}