1use std::sync::Arc;
5use std::time::Duration;
6
7use crate::account_registry::AccountRegistry::{self, AccountRegistryInstance};
8use crate::account_registry::{
9 domain, sign_insert_authenticator, sign_remove_authenticator, sign_update_authenticator,
10};
11use crate::types::{
12 AccountInclusionProof, CreateAccountRequest, GatewayRequestState, GatewayStatusResponse,
13 IndexerErrorCode, IndexerPackedAccountRequest, IndexerPackedAccountResponse,
14 IndexerSignatureNonceRequest, IndexerSignatureNonceResponse, InsertAuthenticatorRequest,
15 RemoveAuthenticatorRequest, RpRequest, ServiceApiError, UpdateAuthenticatorRequest,
16};
17use crate::{Credential, FieldElement, Signer};
18use alloy::primitives::{Address, U256};
19use alloy::providers::{DynProvider, Provider, ProviderBuilder};
20use alloy::uint;
21use ark_babyjubjub::EdwardsAffine;
22use ark_bn254::Bn254;
23use ark_serialize::CanonicalSerialize;
24use circom_types::groth16::Proof;
25use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
26use oprf_types::ShareEpoch;
27use secrecy::ExposeSecret;
28use world_id_primitives::authenticator::AuthenticatorPublicKeySet;
29use world_id_primitives::merkle::MerkleInclusionProof;
30use world_id_primitives::proof::SingleProofInput;
31use world_id_primitives::PrimitiveError;
32pub use world_id_primitives::{authenticator::ProtocolSigner, Config, TREE_DEPTH};
33
34static MASK_RECOVERY_COUNTER: U256 =
35 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
36static MASK_PUBKEY_ID: U256 =
37 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
38static MASK_LEAF_INDEX: U256 =
39 uint!(0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_U256);
40
41const MAX_POLL_TIMEOUT_SECS: u64 = 30;
43
44type UniquenessProof = (Proof<Bn254>, FieldElement);
45
46#[derive(Debug)]
48pub struct Authenticator {
49 pub config: Config,
51 pub packed_account_data: U256,
54 signer: Signer,
55 registry: Option<Arc<AccountRegistryInstance<DynProvider>>>,
56 http_client: reqwest::Client,
57}
58
59impl Authenticator {
60 pub async fn init(seed: &[u8], config: Config) -> Result<Self, AuthenticatorError> {
70 let signer = Signer::from_seed_bytes(seed)?;
71
72 let registry = config.rpc_url().map_or_else(
73 || None,
74 |rpc_url| {
75 let provider = ProviderBuilder::new()
76 .with_chain_id(config.chain_id())
77 .connect_http(rpc_url.clone());
78 Some(AccountRegistry::new(
79 *config.registry_address(),
80 provider.erased(),
81 ))
82 },
83 );
84
85 let http_client = reqwest::Client::new();
86
87 let packed_account_data = Self::get_packed_account_data(
88 signer.onchain_signer_address(),
89 registry.as_ref(),
90 &config,
91 &http_client,
92 )
93 .await?;
94
95 Ok(Self {
96 packed_account_data,
97 signer,
98 config,
99 registry: registry.map(Arc::new),
100 http_client,
101 })
102 }
103
104 pub async fn init_or_create(
113 seed: &[u8],
114 config: Config,
115 recovery_address: Option<Address>,
116 ) -> Result<Option<Self>, AuthenticatorError> {
117 match Self::init(seed, config.clone()).await {
119 Ok(authenticator) => Ok(Some(authenticator)),
120 Err(AuthenticatorError::AccountDoesNotExist) => {
121 let http_client = reqwest::Client::new();
122 Self::create_account(seed, &config, recovery_address, &http_client).await?;
123 Ok(None)
124 }
125 Err(e) => Err(e),
126 }
127 }
128
129 pub async fn init_or_create_blocking(
137 seed: &[u8],
138 config: Config,
139 recovery_address: Option<Address>,
140 ) -> Result<Self, AuthenticatorError> {
141 match Self::init(seed, config.clone()).await {
142 Ok(authenticator) => return Ok(authenticator),
143 Err(AuthenticatorError::AccountDoesNotExist) => {
144 }
146 Err(e) => return Err(e),
147 }
148
149 let http_client = reqwest::Client::new();
151
152 let request_id =
154 Self::create_account(seed, &config, recovery_address, &http_client).await?;
155
156 let start = std::time::Instant::now();
158 let mut delay_ms = 100u64; loop {
161 if start.elapsed().as_secs() >= MAX_POLL_TIMEOUT_SECS {
163 return Err(AuthenticatorError::Timeout(MAX_POLL_TIMEOUT_SECS));
164 }
165
166 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
168
169 match Self::poll_gateway_status(&config, &request_id, &http_client).await {
171 Ok(GatewayRequestState::Finalized { .. }) => {
172 return Self::init(seed, config).await;
173 }
174 Ok(GatewayRequestState::Failed { error }) => {
175 return Err(AuthenticatorError::Generic(format!(
176 "Account creation failed: {error}"
177 )));
178 }
179 Ok(_) => {
180 delay_ms = (delay_ms * 2).min(5000); }
183 Err(e) => return Err(e),
184 }
185 }
186 }
187
188 async fn poll_gateway_status(
194 config: &Config,
195 request_id: &str,
196 http_client: &reqwest::Client,
197 ) -> Result<GatewayRequestState, AuthenticatorError> {
198 let resp = http_client
199 .get(format!("{}/status/{}", config.gateway_url(), request_id))
200 .send()
201 .await?;
202
203 let status = resp.status();
204
205 if status.is_success() {
206 let body: GatewayStatusResponse = resp.json().await?;
207 Ok(body.status)
208 } else {
209 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
210 Err(AuthenticatorError::GatewayError {
211 status: status.as_u16(),
212 body: body_text,
213 })
214 }
215 }
216
217 pub async fn get_packed_account_data(
225 onchain_signer_address: Address,
226 registry: Option<&AccountRegistryInstance<DynProvider>>,
227 config: &Config,
228 http_client: &reqwest::Client,
229 ) -> Result<U256, AuthenticatorError> {
230 let raw_index = if let Some(registry) = registry {
232 registry
233 .authenticatorAddressToPackedAccountData(onchain_signer_address)
234 .call()
235 .await?
236 } else {
237 let url = format!("{}/packed_account", config.indexer_url());
238 let req = IndexerPackedAccountRequest {
239 authenticator_address: onchain_signer_address,
240 };
241 let resp = http_client.post(&url).json(&req).send().await?;
242
243 let status = resp.status();
244 if !status.is_success() {
245 if let Ok(error_resp) = resp.json::<ServiceApiError<IndexerErrorCode>>().await {
247 return match error_resp.code {
248 IndexerErrorCode::AccountDoesNotExist => {
249 Err(AuthenticatorError::AccountDoesNotExist)
250 }
251 _ => Err(AuthenticatorError::IndexerError {
252 status: status.as_u16(),
253 body: error_resp.message,
254 }),
255 };
256 }
257 return Err(AuthenticatorError::IndexerError {
258 status: status.as_u16(),
259 body: "Failed to parse indexer error response".to_string(),
260 });
261 }
262
263 let response: IndexerPackedAccountResponse = resp.json().await?;
264 response.packed_account_data
265 };
266
267 if raw_index == U256::ZERO {
268 return Err(AuthenticatorError::AccountDoesNotExist);
269 }
270
271 Ok(raw_index)
272 }
273
274 #[must_use]
277 pub const fn onchain_address(&self) -> Address {
278 self.signer.onchain_signer_address()
279 }
280
281 #[must_use]
284 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
285 self.signer.offchain_signer_pubkey()
286 }
287
288 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
293 let pk = self.signer.offchain_signer_pubkey().pk;
294 let mut compressed_bytes = Vec::new();
295 pk.serialize_compressed(&mut compressed_bytes)
296 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
297 Ok(U256::from_le_slice(&compressed_bytes))
298 }
299
300 #[must_use]
302 pub fn registry(&self) -> Option<Arc<AccountRegistryInstance<DynProvider>>> {
303 self.registry.clone()
304 }
305
306 #[must_use]
310 pub fn leaf_index(&self) -> U256 {
311 self.packed_account_data & MASK_LEAF_INDEX
312 }
313
314 #[must_use]
318 pub fn recovery_counter(&self) -> U256 {
319 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
320 recovery_counter >> 224
321 }
322
323 #[must_use]
327 pub fn pubkey_id(&self) -> U256 {
328 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
329 pubkey_id >> 192
330 }
331
332 pub async fn fetch_inclusion_proof(
338 &self,
339 ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
340 {
341 let url = format!("{}/proof/{}", self.config.indexer_url(), self.leaf_index());
342 let response = reqwest::get(url).await?;
343 let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
344
345 Ok((response.proof, response.authenticator_pubkeys))
346 }
347
348 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
353 let registry = self.registry();
354 if let Some(registry) = registry {
355 let nonce = registry
356 .leafIndexToSignatureNonce(self.leaf_index())
357 .call()
358 .await?;
359 Ok(nonce)
360 } else {
361 let url = format!("{}/signature_nonce", self.config.indexer_url());
362 let req = IndexerSignatureNonceRequest {
363 leaf_index: self.leaf_index(),
364 };
365 let resp = self.http_client.post(&url).json(&req).send().await?;
366
367 let status = resp.status();
368 if !status.is_success() {
369 return Err(AuthenticatorError::IndexerError {
370 status: status.as_u16(),
371 body: resp
372 .json()
373 .await
374 .unwrap_or_else(|_| "Unable to parse response".to_string()),
375 });
376 }
377
378 let response: IndexerSignatureNonceResponse = resp.json().await?;
379 Ok(response.signature_nonce)
380 }
381 }
382
383 #[allow(clippy::future_not_send)]
390 pub async fn generate_proof(
391 &self,
392 message_hash: FieldElement,
393 rp_request: RpRequest,
394 credential: Credential,
395 ) -> Result<UniquenessProof, AuthenticatorError> {
396 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
397 let key_index = key_set
398 .iter()
399 .position(|pk| pk.pk == self.offchain_pubkey().pk)
400 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
401
402 let query_material = crate::proof::load_embedded_query_material();
404 let nullifier_material = crate::proof::load_embedded_nullifier_material();
405
406 let primitives_rp_id =
408 world_id_primitives::rp::RpId::new(rp_request.rp_id.parse::<u128>().map_err(|e| {
409 PrimitiveError::InvalidInput {
410 attribute: "RP ID".to_string(),
411 reason: format!("invalid RP ID: {e}"),
412 }
413 })?);
414 let primitives_rp_nullifier_key =
415 world_id_primitives::rp::RpNullifierKey::new(rp_request.rp_nullifier_key.inner());
416
417 let args = SingleProofInput::<TREE_DEPTH> {
418 credential,
419 inclusion_proof,
420 key_set,
421 key_index,
422 rp_session_id_r_seed: FieldElement::ZERO, rp_id: primitives_rp_id,
424 share_epoch: ShareEpoch::default().into_inner(), action: rp_request.action_id,
426 nonce: rp_request.nonce,
427 current_timestamp: rp_request.current_time_stamp, rp_signature: rp_request.signature,
429 rp_nullifier_key: primitives_rp_nullifier_key,
430 signal_hash: message_hash,
431 };
432
433 let private_key = self.signer.offchain_signer_private_key().expose_secret();
434
435 let services = self.config.nullifier_oracle_urls();
436 if services.is_empty() {
437 return Err(AuthenticatorError::Generic(
438 "No nullifier oracle URLs configured".to_string(),
439 ));
440 }
441 let requested_threshold = self.config.nullifier_oracle_threshold();
442 if requested_threshold == 0 {
443 return Err(AuthenticatorError::InvalidConfig {
444 attribute: "nullifier_oracle_threshold",
445 reason: "must be at least 1".to_string(),
446 });
447 }
448 let threshold = requested_threshold.min(services.len());
449
450 let mut rng = rand::thread_rng();
451 let (proof, _public, nullifier, _id_commitment) = crate::proof::nullifier(
452 services,
453 threshold,
454 &query_material,
455 &nullifier_material,
456 args,
457 private_key,
458 &mut rng,
459 )
460 .await
461 .map_err(|e| AuthenticatorError::Generic(format!("Failed to generate nullifier: {e}")))?;
462
463 Ok((proof, nullifier.into()))
464 }
465
466 pub async fn insert_authenticator(
475 &mut self,
476 new_authenticator_pubkey: EdDSAPublicKey,
477 new_authenticator_address: Address,
478 ) -> Result<String, AuthenticatorError> {
479 let leaf_index = self.leaf_index();
480 let nonce = self.signing_nonce().await?;
481 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
482 let old_offchain_signer_commitment = Self::leaf_hash(&key_set);
483 key_set.try_push(new_authenticator_pubkey.clone())?;
484 let index = key_set.len() - 1;
485 let new_offchain_signer_commitment = Self::leaf_hash(&key_set);
486
487 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
488
489 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
490
491 #[allow(clippy::cast_possible_truncation)]
492 let signature = sign_insert_authenticator(
494 &self.signer.onchain_signer(),
495 leaf_index,
496 new_authenticator_address,
497 index as u32,
498 encoded_offchain_pubkey,
499 new_offchain_signer_commitment.into(),
500 nonce,
501 &eip712_domain,
502 )
503 .await
504 .map_err(|e| {
505 AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
506 })?;
507
508 #[allow(clippy::cast_possible_truncation)]
509 let req = InsertAuthenticatorRequest {
511 leaf_index,
512 new_authenticator_address,
513 pubkey_id: index as u32,
514 new_authenticator_pubkey: encoded_offchain_pubkey,
515 old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
516 new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
517 sibling_nodes: inclusion_proof
518 .siblings
519 .iter()
520 .map(|s| (*s).into())
521 .collect(),
522 signature: signature.as_bytes().to_vec(),
523 nonce,
524 };
525
526 let resp = self
527 .http_client
528 .post(format!(
529 "{}/insert-authenticator",
530 self.config.gateway_url()
531 ))
532 .json(&req)
533 .send()
534 .await?;
535
536 let status = resp.status();
537 if status.is_success() {
538 let body: GatewayStatusResponse = resp.json().await?;
539 Ok(body.request_id)
540 } else {
541 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
542 Err(AuthenticatorError::GatewayError {
543 status: status.as_u16(),
544 body: body_text,
545 })
546 }
547 }
548
549 pub async fn update_authenticator(
558 &mut self,
559 old_authenticator_address: Address,
560 new_authenticator_address: Address,
561 new_authenticator_pubkey: EdDSAPublicKey,
562 index: u32,
563 ) -> Result<String, AuthenticatorError> {
564 let leaf_index = self.leaf_index();
565 let nonce = self.signing_nonce().await?;
566 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
567 let old_commitment: U256 = Self::leaf_hash(&key_set).into();
568 key_set.try_set_at_index(index as usize, new_authenticator_pubkey.clone())?;
569 let new_commitment: U256 = Self::leaf_hash(&key_set).into();
570
571 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
572
573 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
574
575 let signature = sign_update_authenticator(
576 &self.signer.onchain_signer(),
577 leaf_index,
578 old_authenticator_address,
579 new_authenticator_address,
580 index,
581 encoded_offchain_pubkey,
582 new_commitment,
583 nonce,
584 &eip712_domain,
585 )
586 .await
587 .map_err(|e| {
588 AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
589 })?;
590
591 let sibling_nodes: Vec<U256> = inclusion_proof
592 .siblings
593 .iter()
594 .map(|s| (*s).into())
595 .collect();
596
597 let req = UpdateAuthenticatorRequest {
598 leaf_index,
599 old_authenticator_address,
600 new_authenticator_address,
601 old_offchain_signer_commitment: old_commitment,
602 new_offchain_signer_commitment: new_commitment,
603 sibling_nodes,
604 signature: signature.as_bytes().to_vec(),
605 nonce,
606 pubkey_id: Some(index),
607 new_authenticator_pubkey: Some(encoded_offchain_pubkey),
608 };
609
610 let resp = self
611 .http_client
612 .post(format!(
613 "{}/update-authenticator",
614 self.config.gateway_url()
615 ))
616 .json(&req)
617 .send()
618 .await?;
619
620 let status = resp.status();
621 if status.is_success() {
622 let gateway_resp: GatewayStatusResponse = resp.json().await?;
623 Ok(gateway_resp.request_id)
624 } else {
625 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
626 Err(AuthenticatorError::GatewayError {
627 status: status.as_u16(),
628 body: body_text,
629 })
630 }
631 }
632
633 pub async fn remove_authenticator(
642 &mut self,
643 authenticator_address: Address,
644 index: u32,
645 ) -> Result<String, AuthenticatorError> {
646 let leaf_index = self.leaf_index();
647 let nonce = self.signing_nonce().await?;
648 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
649 let old_commitment: U256 = Self::leaf_hash(&key_set).into();
650 let existing_pubkey = key_set
651 .get(index as usize)
652 .ok_or(AuthenticatorError::PublicKeyNotFound)?;
653
654 let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
655
656 key_set[index as usize] = EdDSAPublicKey {
657 pk: EdwardsAffine::default(),
658 };
659 let new_commitment: U256 = Self::leaf_hash(&key_set).into();
660
661 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
662
663 let signature = sign_remove_authenticator(
664 &self.signer.onchain_signer(),
665 leaf_index,
666 authenticator_address,
667 index,
668 encoded_old_offchain_pubkey,
669 new_commitment,
670 nonce,
671 &eip712_domain,
672 )
673 .await
674 .map_err(|e| {
675 AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
676 })?;
677
678 let sibling_nodes: Vec<U256> = inclusion_proof
679 .siblings
680 .iter()
681 .map(|s| (*s).into())
682 .collect();
683
684 let req = RemoveAuthenticatorRequest {
685 leaf_index,
686 authenticator_address,
687 old_offchain_signer_commitment: old_commitment,
688 new_offchain_signer_commitment: new_commitment,
689 sibling_nodes,
690 signature: signature.as_bytes().to_vec(),
691 nonce,
692 pubkey_id: Some(index),
693 authenticator_pubkey: Some(encoded_old_offchain_pubkey),
694 };
695
696 let resp = self
697 .http_client
698 .post(format!(
699 "{}/remove-authenticator",
700 self.config.gateway_url()
701 ))
702 .json(&req)
703 .send()
704 .await?;
705
706 let status = resp.status();
707 if status.is_success() {
708 let gateway_resp: GatewayStatusResponse = resp.json().await?;
709 Ok(gateway_resp.request_id)
710 } else {
711 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
712 Err(AuthenticatorError::GatewayError {
713 status: status.as_u16(),
714 body: body_text,
715 })
716 }
717 }
718
719 #[allow(clippy::missing_panics_doc)]
726 #[must_use]
727 pub fn leaf_hash(key_set: &AuthenticatorPublicKeySet) -> ark_babyjubjub::Fq {
728 key_set.leaf_hash()
729 }
730
731 async fn create_account(
737 seed: &[u8],
738 config: &Config,
739 recovery_address: Option<Address>,
740 http_client: &reqwest::Client,
741 ) -> Result<String, AuthenticatorError> {
742 let signer = Signer::from_seed_bytes(seed)?;
743
744 let mut key_set = AuthenticatorPublicKeySet::new(None)?;
745 key_set.try_push(signer.offchain_signer_pubkey())?;
746 let leaf_hash = Self::leaf_hash(&key_set);
747
748 let offchain_pubkey_compressed = {
749 let pk = signer.offchain_signer_pubkey().pk;
750 let mut compressed_bytes = Vec::new();
751 pk.serialize_compressed(&mut compressed_bytes)
752 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
753 U256::from_le_slice(&compressed_bytes)
754 };
755
756 let req = CreateAccountRequest {
757 recovery_address,
758 authenticator_addresses: vec![signer.onchain_signer_address()],
759 authenticator_pubkeys: vec![offchain_pubkey_compressed],
760 offchain_signer_commitment: leaf_hash.into(),
761 };
762
763 let resp = http_client
764 .post(format!("{}/create-account", config.gateway_url()))
765 .json(&req)
766 .send()
767 .await?;
768
769 let status = resp.status();
770 if status.is_success() {
771 let body: GatewayStatusResponse = resp.json().await?;
772 Ok(body.request_id)
773 } else {
774 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
775 Err(AuthenticatorError::GatewayError {
776 status: status.as_u16(),
777 body: body_text,
778 })
779 }
780 }
781}
782
783impl ProtocolSigner for Authenticator {
784 fn sign(&self, message: FieldElement) -> EdDSASignature {
785 self.signer
786 .offchain_signer_private_key()
787 .expose_secret()
788 .sign(*message)
789 }
790}
791
792pub trait OnchainKeyRepresentable {
794 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
801}
802
803impl OnchainKeyRepresentable for EdDSAPublicKey {
804 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
806 let mut compressed_bytes = Vec::new();
807 self.pk
808 .serialize_compressed(&mut compressed_bytes)
809 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
810 Ok(U256::from_le_slice(&compressed_bytes))
811 }
812}
813
814#[derive(Debug, thiserror::Error)]
816pub enum AuthenticatorError {
817 #[error(transparent)]
819 PrimitiveError(#[from] PrimitiveError),
820
821 #[error("Account is not registered for this authenticator.")]
824 AccountDoesNotExist,
825
826 #[error("Account already exists for this authenticator.")]
828 AccountAlreadyExists,
829
830 #[error("Error interacting with EVM contract: {0}")]
832 ContractError(#[from] alloy::contract::Error),
833
834 #[error("Network error: {0}")]
836 NetworkError(#[from] reqwest::Error),
837
838 #[error("Public key not found.")]
840 PublicKeyNotFound,
841
842 #[error("Gateway error (status {status}): {body}")]
844 GatewayError {
845 status: u16,
847 body: String,
849 },
850
851 #[error("Indexer error (status {status}): {body}")]
853 IndexerError {
854 status: u16,
856 body: String,
858 },
859
860 #[error("Account creation timed out after {0} seconds")]
862 Timeout(u64),
863
864 #[error("Invalid configuration for {attribute}: {reason}")]
866 InvalidConfig {
867 attribute: &'static str,
869 reason: String,
871 },
872
873 #[error("{0}")]
875 Generic(String),
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881 use alloy::primitives::{address, U256};
882
883 #[tokio::test]
886 async fn test_get_packed_account_data_from_indexer() {
887 let mut server = mockito::Server::new_async().await;
888 let indexer_url = server.url();
889
890 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
891 let expected_packed_index = U256::from(42);
892
893 let mock = server
894 .mock("POST", "/packed_account")
895 .match_header("content-type", "application/json")
896 .match_body(mockito::Matcher::JsonString(
897 serde_json::json!({
898 "authenticator_address": test_address
899 })
900 .to_string(),
901 ))
902 .with_status(200)
903 .with_header("content-type", "application/json")
904 .with_body(
905 serde_json::json!({
906 "packed_account_data": format!("{:#x}", expected_packed_index)
907 })
908 .to_string(),
909 )
910 .create_async()
911 .await;
912
913 let config = Config::new(
914 None,
915 1,
916 address!("0x0000000000000000000000000000000000000001"),
917 indexer_url,
918 "http://gateway.example.com".to_string(),
919 Vec::new(),
920 2,
921 )
922 .unwrap();
923
924 let http_client = reqwest::Client::new();
925
926 let result = Authenticator::get_packed_account_data(
927 test_address,
928 None, &config,
930 &http_client,
931 )
932 .await
933 .unwrap();
934
935 assert_eq!(result, expected_packed_index);
936 mock.assert_async().await;
937 drop(server);
938 }
939
940 #[tokio::test]
941 async fn test_get_packed_account_data_from_indexer_error() {
942 let mut server = mockito::Server::new_async().await;
943 let indexer_url = server.url();
944
945 let test_address = address!("0x0000000000000000000000000000000000000099");
946
947 let mock = server
948 .mock("POST", "/packed_account")
949 .with_status(400)
950 .with_header("content-type", "application/json")
951 .with_body(
952 serde_json::json!({
953 "code": "account_does_not_exist",
954 "message": "There is no account for this authenticator address"
955 })
956 .to_string(),
957 )
958 .create_async()
959 .await;
960
961 let config = Config::new(
962 None,
963 1,
964 address!("0x0000000000000000000000000000000000000001"),
965 indexer_url,
966 "http://gateway.example.com".to_string(),
967 Vec::new(),
968 2,
969 )
970 .unwrap();
971
972 let http_client = reqwest::Client::new();
973
974 let result =
975 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
976
977 assert!(matches!(
978 result,
979 Err(AuthenticatorError::AccountDoesNotExist)
980 ));
981 mock.assert_async().await;
982 drop(server);
983 }
984
985 #[tokio::test]
986 async fn test_signing_nonce_from_indexer() {
987 let mut server = mockito::Server::new_async().await;
988 let indexer_url = server.url();
989
990 let leaf_index = U256::from(1);
991 let expected_nonce = U256::from(5);
992
993 let mock = server
994 .mock("POST", "/signature_nonce")
995 .match_header("content-type", "application/json")
996 .match_body(mockito::Matcher::JsonString(
997 serde_json::json!({
998 "leaf_index": format!("{:#x}", leaf_index)
999 })
1000 .to_string(),
1001 ))
1002 .with_status(200)
1003 .with_header("content-type", "application/json")
1004 .with_body(
1005 serde_json::json!({
1006 "signature_nonce": format!("{:#x}", expected_nonce)
1007 })
1008 .to_string(),
1009 )
1010 .create_async()
1011 .await;
1012
1013 let config = Config::new(
1014 None,
1015 1,
1016 address!("0x0000000000000000000000000000000000000001"),
1017 indexer_url,
1018 "http://gateway.example.com".to_string(),
1019 Vec::new(),
1020 2,
1021 )
1022 .unwrap();
1023
1024 let authenticator = Authenticator {
1025 config,
1026 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1028 registry: None, http_client: reqwest::Client::new(),
1030 };
1031
1032 let nonce = authenticator.signing_nonce().await.unwrap();
1033
1034 assert_eq!(nonce, expected_nonce);
1035 mock.assert_async().await;
1036 drop(server);
1037 }
1038
1039 #[tokio::test]
1040 async fn test_signing_nonce_from_indexer_error() {
1041 let mut server = mockito::Server::new_async().await;
1042 let indexer_url = server.url();
1043
1044 let mock = server
1045 .mock("POST", "/signature_nonce")
1046 .with_status(400)
1047 .with_header("content-type", "application/json")
1048 .with_body(
1049 serde_json::json!({
1050 "code": "invalid_leaf_index",
1051 "message": "Account index cannot be zero"
1052 })
1053 .to_string(),
1054 )
1055 .create_async()
1056 .await;
1057
1058 let config = Config::new(
1059 None,
1060 1,
1061 address!("0x0000000000000000000000000000000000000001"),
1062 indexer_url,
1063 "http://gateway.example.com".to_string(),
1064 Vec::new(),
1065 2,
1066 )
1067 .unwrap();
1068
1069 let authenticator = Authenticator {
1070 config,
1071 packed_account_data: U256::ZERO,
1072 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1073 registry: None,
1074 http_client: reqwest::Client::new(),
1075 };
1076
1077 let result = authenticator.signing_nonce().await;
1078
1079 assert!(matches!(
1080 result,
1081 Err(AuthenticatorError::IndexerError { .. })
1082 ));
1083 mock.assert_async().await;
1084 drop(server);
1085 }
1086}