Skip to main content

world_id_authenticator/
authenticator.rs

1//! This module contains all the base functionality to support Authenticators in World ID.
2//!
3//! An Authenticator is the application layer with which a user interacts with the Protocol.
4
5use std::sync::Arc;
6
7use crate::api_types::{
8    AccountInclusionProof, CreateAccountRequest, GatewayRequestState, GatewayStatusResponse,
9    IndexerAuthenticatorPubkeysResponse, IndexerErrorCode, IndexerPackedAccountRequest,
10    IndexerPackedAccountResponse, IndexerQueryRequest, IndexerSignatureNonceResponse,
11    InsertAuthenticatorRequest, RemoveAuthenticatorRequest, ServiceApiError,
12    UpdateAuthenticatorRequest,
13};
14use world_id_primitives::{
15    Credential, FieldElement, ProofRequest, RequestItem, ResponseItem, SessionNullifier, Signer,
16};
17
18use crate::registry::{
19    WorldIdRegistry::WorldIdRegistryInstance, domain, sign_insert_authenticator,
20    sign_remove_authenticator, sign_update_authenticator,
21};
22use alloy::{
23    primitives::{Address, U256},
24    providers::DynProvider,
25    uint,
26};
27use ark_serialize::CanonicalSerialize;
28use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
29use groth16_material::circom::CircomGroth16Material;
30use reqwest::StatusCode;
31use secrecy::ExposeSecret;
32use taceo_oprf::client::Connector;
33pub use world_id_primitives::{Config, TREE_DEPTH, authenticator::ProtocolSigner};
34use world_id_primitives::{
35    PrimitiveError, ZeroKnowledgeProof,
36    authenticator::{
37        AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError,
38        decode_sparse_authenticator_pubkeys,
39    },
40    merkle::MerkleInclusionProof,
41};
42use world_id_proof::{
43    AuthenticatorProofInput,
44    credential_blinding_factor::OprfCredentialBlindingFactor,
45    nullifier::OprfNullifier,
46    proof::{ProofError, generate_nullifier_proof},
47};
48
49static MASK_RECOVERY_COUNTER: U256 =
50    uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
51static MASK_PUBKEY_ID: U256 =
52    uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
53static MASK_LEAF_INDEX: U256 =
54    uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
55
56/// An Authenticator is the base layer with which a user interacts with the Protocol.
57pub struct Authenticator {
58    /// General configuration for the Authenticator.
59    pub config: Config,
60    /// The packed account data for the holder's World ID is a `uint256` defined in the `WorldIDRegistry` contract as:
61    /// `recovery_counter` (32 bits) | `pubkey_id` (commitment to all off-chain public keys) (32 bits) | `leaf_index` (192 bits)
62    pub packed_account_data: U256,
63    signer: Signer,
64    registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
65    http_client: reqwest::Client,
66    ws_connector: Connector,
67    query_material: Arc<CircomGroth16Material>,
68    nullifier_material: Arc<CircomGroth16Material>,
69}
70
71#[expect(clippy::missing_fields_in_debug)]
72impl std::fmt::Debug for Authenticator {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("Authenticator")
75            .field("config", &self.config)
76            .field("packed_account_data", &self.packed_account_data)
77            .field("signer", &self.signer)
78            .finish()
79    }
80}
81
82impl Authenticator {
83    async fn response_body_or_fallback(response: reqwest::Response) -> String {
84        response
85            .text()
86            .await
87            .unwrap_or_else(|e| format!("Unable to read response body: {e}"))
88    }
89
90    /// Initialize an Authenticator from a seed and config.
91    ///
92    /// This method will error if the World ID account does not exist on the registry.
93    ///
94    /// # Errors
95    /// - Will error if the provided seed is invalid (not 32 bytes).
96    /// - Will error if the RPC URL is invalid.
97    /// - Will error if there are contract call failures.
98    /// - Will error if the account does not exist (`AccountDoesNotExist`).
99    pub async fn init(
100        seed: &[u8],
101        config: Config,
102        query_material: Arc<CircomGroth16Material>,
103        nullifier_material: Arc<CircomGroth16Material>,
104    ) -> Result<Self, AuthenticatorError> {
105        let signer = Signer::from_seed_bytes(seed)?;
106
107        let registry = config.rpc_url().map_or_else(
108            || None,
109            |rpc_url| {
110                let provider = alloy::providers::ProviderBuilder::new()
111                    .with_chain_id(config.chain_id())
112                    .connect_http(rpc_url.clone());
113                Some(crate::registry::WorldIdRegistry::new(
114                    *config.registry_address(),
115                    alloy::providers::Provider::erased(provider),
116                ))
117            },
118        );
119
120        let http_client = reqwest::Client::new();
121
122        let packed_account_data = Self::get_packed_account_data(
123            signer.onchain_signer_address(),
124            registry.as_ref(),
125            &config,
126            &http_client,
127        )
128        .await?;
129
130        let mut root_store = rustls::RootCertStore::empty();
131        root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
132        let rustls_config = rustls::ClientConfig::builder()
133            .with_root_certificates(root_store)
134            .with_no_client_auth();
135        let ws_connector = Connector::Rustls(Arc::new(rustls_config));
136
137        Ok(Self {
138            packed_account_data,
139            signer,
140            config,
141            registry: registry.map(Arc::new),
142            http_client,
143            ws_connector,
144            query_material,
145            nullifier_material,
146        })
147    }
148
149    /// Registers a new World ID in the `WorldIDRegistry`.
150    ///
151    /// Given the registration process is asynchronous, this method will return a `InitializingAuthenticator`
152    /// object.
153    ///
154    /// # Errors
155    /// - See `init` for additional error details.
156    pub async fn register(
157        seed: &[u8],
158        config: Config,
159        recovery_address: Option<Address>,
160    ) -> Result<InitializingAuthenticator, AuthenticatorError> {
161        let http_client = reqwest::Client::new();
162        InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
163    }
164
165    /// Initializes (if the World ID already exists in the registry) or registers a new World ID.
166    ///
167    /// The registration process is asynchronous and may take some time. This method will block
168    /// the thread until the registration is in a final state (success or terminal error). For better
169    /// user experience in end authenticator clients, it is recommended to implement custom polling logic.
170    ///
171    /// Explicit `init` or `register` calls are also recommended as the authenticator should know
172    /// if a new World ID should be truly created. For example, an authenticator may have been revoked
173    /// access to an existing World ID.
174    ///
175    /// # Errors
176    /// - See `init` for additional error details.
177    pub async fn init_or_register(
178        seed: &[u8],
179        config: Config,
180        query_material: Arc<CircomGroth16Material>,
181        nullifier_material: Arc<CircomGroth16Material>,
182        recovery_address: Option<Address>,
183    ) -> Result<Self, AuthenticatorError> {
184        match Self::init(
185            seed,
186            config.clone(),
187            query_material.clone(),
188            nullifier_material.clone(),
189        )
190        .await
191        {
192            Ok(authenticator) => Ok(authenticator),
193            Err(AuthenticatorError::AccountDoesNotExist) => {
194                // Authenticator is not registered, create it.
195                let http_client = reqwest::Client::new();
196                let initializing_authenticator = InitializingAuthenticator::new(
197                    seed,
198                    config.clone(),
199                    recovery_address,
200                    http_client,
201                )
202                .await?;
203
204                let backoff = backon::ExponentialBuilder::default()
205                    .with_min_delay(std::time::Duration::from_millis(800))
206                    .with_factor(1.5)
207                    .without_max_times()
208                    .with_total_delay(Some(std::time::Duration::from_secs(120)));
209
210                let poller = || async {
211                    let poll_status = initializing_authenticator.poll_status().await;
212                    let result = match poll_status {
213                        Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
214                        Ok(GatewayRequestState::Failed { error_code, error }) => Err(
215                            PollResult::TerminalError(AuthenticatorError::RegistrationError {
216                                error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
217                                error_message: error,
218                            }),
219                        ),
220                        Err(AuthenticatorError::GatewayError { status, body }) => {
221                            if status.is_client_error() {
222                                Err(PollResult::TerminalError(
223                                    AuthenticatorError::GatewayError { status, body },
224                                ))
225                            } else {
226                                Err(PollResult::Retryable)
227                            }
228                        }
229                        _ => Err(PollResult::Retryable),
230                    };
231
232                    match result {
233                        Ok(()) => match Self::init(
234                            seed,
235                            config.clone(),
236                            query_material.clone(),
237                            nullifier_material.clone(),
238                        )
239                        .await
240                        {
241                            Ok(auth) => Ok(auth),
242                            Err(AuthenticatorError::AccountDoesNotExist) => {
243                                Err(PollResult::Retryable)
244                            }
245                            Err(e) => Err(PollResult::TerminalError(e)),
246                        },
247                        Err(e) => Err(e),
248                    }
249                };
250
251                let result = backon::Retryable::retry(poller, backoff)
252                    .when(|e| matches!(e, PollResult::Retryable))
253                    .await;
254
255                match result {
256                    Ok(authenticator) => Ok(authenticator),
257                    Err(PollResult::TerminalError(e)) => Err(e),
258                    Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
259                }
260            }
261            Err(e) => Err(e),
262        }
263    }
264
265    /// Returns the packed account data for the holder's World ID.
266    ///
267    /// The packed account data is a 256 bit integer which includes the World ID's leaf index, their recovery counter,
268    /// and their pubkey id/commitment.
269    ///
270    /// # Errors
271    /// Will error if the network call fails or if the account does not exist.
272    pub async fn get_packed_account_data(
273        onchain_signer_address: Address,
274        registry: Option<&WorldIdRegistryInstance<DynProvider>>,
275        config: &Config,
276        http_client: &reqwest::Client,
277    ) -> Result<U256, AuthenticatorError> {
278        // If the registry is available through direct RPC calls, use it. Otherwise fallback to the indexer.
279        let raw_index = if let Some(registry) = registry {
280            // TODO: Better error handling to expose the specific failure
281            registry
282                .getPackedAccountData(onchain_signer_address)
283                .call()
284                .await?
285        } else {
286            let url = format!("{}/packed-account", config.indexer_url());
287            let req = IndexerPackedAccountRequest {
288                authenticator_address: onchain_signer_address,
289            };
290            let resp = http_client.post(&url).json(&req).send().await?;
291            let status = resp.status();
292            if !status.is_success() {
293                let body = Self::response_body_or_fallback(resp).await;
294                if let Ok(error_resp) =
295                    serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
296                {
297                    return match error_resp.code {
298                        IndexerErrorCode::AccountDoesNotExist => {
299                            Err(AuthenticatorError::AccountDoesNotExist)
300                        }
301                        _ => Err(AuthenticatorError::IndexerError {
302                            status,
303                            body: error_resp.message,
304                        }),
305                    };
306                }
307                return Err(AuthenticatorError::IndexerError { status, body });
308            }
309
310            let response: IndexerPackedAccountResponse = resp.json().await?;
311            response.packed_account_data
312        };
313
314        if raw_index == U256::ZERO {
315            return Err(AuthenticatorError::AccountDoesNotExist);
316        }
317
318        Ok(raw_index)
319    }
320
321    /// Returns the k256 public key of the Authenticator signer which is used to verify on-chain operations,
322    /// chiefly with the `WorldIdRegistry` contract.
323    #[must_use]
324    pub const fn onchain_address(&self) -> Address {
325        self.signer.onchain_signer_address()
326    }
327
328    /// Returns the `EdDSA` public key of the Authenticator signer which is used to verify off-chain operations. For example,
329    /// the Nullifier Oracle uses it to verify requests for nullifiers.
330    #[must_use]
331    pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
332        self.signer.offchain_signer_pubkey()
333    }
334
335    /// Returns the compressed `EdDSA` public key of the Authenticator signer which is used to verify off-chain operations.
336    /// For example, the Nullifier Oracle uses it to verify requests for nullifiers.
337    /// # Errors
338    /// Will error if the public key cannot be serialized.
339    pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
340        let pk = self.signer.offchain_signer_pubkey().pk;
341        let mut compressed_bytes = Vec::new();
342        pk.serialize_compressed(&mut compressed_bytes)
343            .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
344        Ok(U256::from_le_slice(&compressed_bytes))
345    }
346
347    /// Returns a reference to the `WorldIdRegistry` contract instance.
348    #[must_use]
349    pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
350        self.registry.clone()
351    }
352
353    /// Returns the index for the holder's World ID.
354    ///
355    /// # Definition
356    ///
357    /// The `leaf_index` is the main (internal) identifier of a World ID. It is registered in
358    /// the `WorldIDRegistry` and represents the index at the Merkle tree where the World ID
359    /// resides.
360    ///
361    /// # Notes
362    /// - The `leaf_index` is used as input in the nullifier generation, ensuring a nullifier
363    ///   will always be the same for the same RP context and the same World ID (allowing for uniqueness).
364    /// - The `leaf_index` is generally not exposed outside Authenticators. It is not a secret because
365    ///   it's not exposed to RPs outside ZK-circuits, but the only acceptable exposure outside an Authenticator
366    ///   is to fetch Merkle inclusion proofs from an indexer or it may create a pseudonymous identifier.
367    /// - The `leaf_index` is stored as a `uint64` inside packed account data.
368    #[must_use]
369    pub fn leaf_index(&self) -> u64 {
370        (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
371    }
372
373    /// Returns the recovery counter for the holder's World ID.
374    ///
375    /// The recovery counter is used to efficiently invalidate all the old keys when an account is recovered.
376    #[must_use]
377    pub fn recovery_counter(&self) -> U256 {
378        let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
379        recovery_counter >> 224
380    }
381
382    /// Returns the pubkey id (or commitment) for the holder's World ID.
383    ///
384    /// This is a commitment to all the off-chain public keys that are authorized to act on behalf of the holder.
385    #[must_use]
386    pub fn pubkey_id(&self) -> U256 {
387        let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
388        pubkey_id >> 192
389    }
390
391    /// Fetches a Merkle inclusion proof for the holder's World ID given their account index.
392    ///
393    /// # Errors
394    /// - Will error if the provided indexer URL is not valid or if there are HTTP call failures.
395    /// - Will error if the user is not registered on the `WorldIDRegistry`.
396    pub async fn fetch_inclusion_proof(
397        &self,
398    ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
399    {
400        let url = format!("{}/inclusion-proof", self.config.indexer_url());
401        let req = IndexerQueryRequest {
402            leaf_index: self.leaf_index(),
403        };
404        let response = self.http_client.post(&url).json(&req).send().await?;
405        let status = response.status();
406        if !status.is_success() {
407            return Err(AuthenticatorError::IndexerError {
408                status,
409                body: Self::response_body_or_fallback(response).await,
410            });
411        }
412        let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
413
414        Ok((response.inclusion_proof, response.authenticator_pubkeys))
415    }
416
417    /// Fetches the current authenticator public key set for the account.
418    ///
419    /// This is used by mutation operations to compute old/new offchain signer commitments
420    /// without requiring Merkle proof generation.
421    ///
422    /// # Errors
423    /// - Will error if the provided indexer URL is not valid or if there are HTTP call failures.
424    /// - Will error if the user is not registered on the `WorldIDRegistry`.
425    pub async fn fetch_authenticator_pubkeys(
426        &self,
427    ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
428        let url = format!("{}/authenticator-pubkeys", self.config.indexer_url());
429        let req = IndexerQueryRequest {
430            leaf_index: self.leaf_index(),
431        };
432        let response = self.http_client.post(&url).json(&req).send().await?;
433        let status = response.status();
434        if !status.is_success() {
435            return Err(AuthenticatorError::IndexerError {
436                status,
437                body: Self::response_body_or_fallback(response).await,
438            });
439        }
440        let response = response
441            .json::<IndexerAuthenticatorPubkeysResponse>()
442            .await?;
443        Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
444    }
445
446    /// Returns the signing nonce for the holder's World ID.
447    ///
448    /// # Errors
449    /// Will return an error if the registry contract call fails.
450    pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
451        let registry = self.registry();
452        if let Some(registry) = registry {
453            let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
454            Ok(nonce)
455        } else {
456            let url = format!("{}/signature-nonce", self.config.indexer_url());
457            let req = IndexerQueryRequest {
458                leaf_index: self.leaf_index(),
459            };
460            let resp = self.http_client.post(&url).json(&req).send().await?;
461
462            let status = resp.status();
463            if !status.is_success() {
464                return Err(AuthenticatorError::IndexerError {
465                    status,
466                    body: Self::response_body_or_fallback(resp).await,
467                });
468            }
469
470            let response: IndexerSignatureNonceResponse = resp.json().await?;
471            Ok(response.signature_nonce)
472        }
473    }
474
475    /// Checks that the OPRF Nodes configuration is valid and returns the list of URLs and the threshold to use.
476    ///
477    /// # Errors
478    /// Will return an error if there are no OPRF Nodes configured or if the threshold is invalid.
479    fn check_oprf_config(&self) -> Result<(&[String], usize), AuthenticatorError> {
480        let services = self.config.nullifier_oracle_urls();
481        if services.is_empty() {
482            return Err(AuthenticatorError::Generic(
483                "No nullifier oracle URLs configured".to_string(),
484            ));
485        }
486        let requested_threshold = self.config.nullifier_oracle_threshold();
487        if requested_threshold == 0 {
488            return Err(AuthenticatorError::InvalidConfig {
489                attribute: "nullifier_oracle_threshold",
490                reason: "must be at least 1".to_string(),
491            });
492        }
493        let threshold = requested_threshold.min(services.len());
494        Ok((services, threshold))
495    }
496
497    fn decode_indexer_pubkeys(
498        pubkeys: Vec<Option<U256>>,
499    ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
500        decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
501            SparseAuthenticatorPubkeysError::SlotOutOfBounds {
502                slot_index,
503                max_supported_slot,
504            } => AuthenticatorError::InvalidIndexerPubkeySlot {
505                slot_index,
506                max_supported_slot,
507            },
508            SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
509                PrimitiveError::Deserialization(format!(
510                    "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
511                ))
512                .into()
513            }
514        })
515    }
516
517    fn insert_or_reuse_authenticator_key(
518        key_set: &mut AuthenticatorPublicKeySet,
519        new_authenticator_pubkey: EdDSAPublicKey,
520    ) -> Result<usize, AuthenticatorError> {
521        if let Some(index) = key_set.iter().position(Option::is_none) {
522            key_set.try_set_at_index(index, new_authenticator_pubkey)?;
523            Ok(index)
524        } else {
525            key_set.try_push(new_authenticator_pubkey)?;
526            Ok(key_set.len() - 1)
527        }
528    }
529
530    /// Generates a nullifier for a World ID Proof (through OPRF Nodes).
531    ///
532    /// A nullifier is a unique, one-time use, anonymous identifier for a World ID
533    /// on a specific RP context. It is used to ensure that a single World ID can only
534    /// perform an action once.
535    ///
536    /// # Errors
537    ///
538    /// - Will raise a [`ProofError`] if there is any issue generating the nullifier. For example,
539    ///   network issues, unexpected incorrect responses from OPRF Nodes.
540    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
541    pub async fn generate_nullifier(
542        &self,
543        proof_request: &ProofRequest,
544        inclusion_proof: MerkleInclusionProof<TREE_DEPTH>,
545        key_set: AuthenticatorPublicKeySet,
546    ) -> Result<OprfNullifier, AuthenticatorError> {
547        let (services, threshold) = self.check_oprf_config()?;
548        let key_index = key_set
549            .iter()
550            .position(|pk| {
551                pk.as_ref()
552                    .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
553            })
554            .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
555
556        let authenticator_input = AuthenticatorProofInput::new(
557            key_set,
558            inclusion_proof,
559            self.signer
560                .offchain_signer_private_key()
561                .expose_secret()
562                .clone(),
563            key_index,
564        );
565
566        Ok(OprfNullifier::generate(
567            services,
568            threshold,
569            &self.query_material,
570            authenticator_input,
571            proof_request,
572            self.ws_connector.clone(),
573        )
574        .await?)
575    }
576
577    // TODO add more docs
578    /// Generates a blinding factor for a Credential sub (through OPRF Nodes).
579    ///
580    /// # Errors
581    ///
582    /// - Will raise a [`ProofError`] if there is any issue generating the blinding factor.
583    ///   For example, network issues, unexpected incorrect responses from OPRF Nodes.
584    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
585    pub async fn generate_credential_blinding_factor(
586        &self,
587        issuer_schema_id: u64,
588    ) -> Result<FieldElement, AuthenticatorError> {
589        let (services, threshold) = self.check_oprf_config()?;
590
591        let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
592        let key_index = key_set
593            .iter()
594            .position(|pk| {
595                pk.as_ref()
596                    .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
597            })
598            .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
599
600        let authenticator_input = AuthenticatorProofInput::new(
601            key_set,
602            inclusion_proof,
603            self.signer
604                .offchain_signer_private_key()
605                .expose_secret()
606                .clone(),
607            key_index,
608        );
609
610        let blinding_factor = OprfCredentialBlindingFactor::generate(
611            services,
612            threshold,
613            &self.query_material,
614            authenticator_input,
615            issuer_schema_id,
616            FieldElement::ZERO, // for now action is always zero, might change in future
617            self.ws_connector.clone(),
618        )
619        .await?;
620
621        Ok(blinding_factor.verifiable_oprf_output.output.into())
622    }
623
624    /// Generates a single World ID Proof from a provided `[ProofRequest]` and `[Credential]`. This
625    /// method generates the raw proof to be translated into a Uniqueness Proof or a Session Proof for the RP.
626    ///
627    /// This assumes the RP's `[ProofRequest]` has already been parsed to determine
628    /// which `[Credential]` is appropriate for the request. This method responds to a
629    /// specific `[RequestItem]` (a `[ProofRequest]` may contain multiple items).
630    ///
631    /// # Arguments
632    /// - `oprf_nullifier`: The `[OprfNullifier]` output generated from the `generate_nullifier` function.
633    /// - `request_item`: The specific `RequestItem` that is being resolved from the RP's `ProofRequest`.
634    /// - `credential`: The Credential to be used for the proof that fulfills the `RequestItem`.
635    /// - `credential_sub_blinding_factor`: The blinding factor for the Credential's sub.
636    /// - `session_id_r_seed`: The session ID random seed. Obtained from the RP's [`ProofRequest`].
637    /// - `session_id`: The expected session ID provided by the RP. Only needed for Session Proofs. Obtained from the RP's [`ProofRequest`].
638    /// - `request_timestamp`: The timestamp of the request. Obtained from the RP's [`ProofRequest`].
639    ///
640    /// # Errors
641    /// - Will error if the any of the provided parameters are not valid.
642    /// - Will error if any of the required network requests fail.
643    /// - Will error if the user does not have a registered World ID.
644    #[allow(clippy::too_many_arguments)]
645    pub fn generate_single_proof(
646        &self,
647        oprf_nullifier: OprfNullifier,
648        request_item: &RequestItem,
649        credential: &Credential,
650        credential_sub_blinding_factor: FieldElement,
651        session_id_r_seed: FieldElement,
652        session_id: Option<FieldElement>,
653        request_timestamp: u64,
654    ) -> Result<ResponseItem, AuthenticatorError> {
655        let mut rng = rand::rngs::OsRng;
656
657        let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
658        let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
659
660        let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
661
662        let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
663            &self.nullifier_material,
664            &mut rng,
665            credential,
666            credential_sub_blinding_factor,
667            oprf_nullifier,
668            request_item,
669            session_id,
670            session_id_r_seed,
671            expires_at_min,
672        )?;
673
674        let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
675
676        // Construct the appropriate response item based on proof type
677        let nullifier_fe: FieldElement = nullifier.into();
678        let response_item = if session_id.is_some() {
679            let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query);
680            ResponseItem::new_session(
681                request_item.identifier.clone(),
682                request_item.issuer_schema_id,
683                proof,
684                session_nullifier,
685                expires_at_min,
686            )
687        } else {
688            ResponseItem::new_uniqueness(
689                request_item.identifier.clone(),
690                request_item.issuer_schema_id,
691                proof,
692                nullifier_fe,
693                expires_at_min,
694            )
695        };
696
697        Ok(response_item)
698    }
699
700    /// Inserts a new authenticator to the account.
701    ///
702    /// # Errors
703    /// Will error if the provided RPC URL is not valid or if there are HTTP call failures.
704    ///
705    /// # Note
706    /// TODO: After successfully inserting an authenticator, the `packed_account_data` should be
707    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
708    pub async fn insert_authenticator(
709        &mut self,
710        new_authenticator_pubkey: EdDSAPublicKey,
711        new_authenticator_address: Address,
712    ) -> Result<String, AuthenticatorError> {
713        let leaf_index = self.leaf_index();
714        let nonce = self.signing_nonce().await?;
715        let mut key_set = self.fetch_authenticator_pubkeys().await?;
716        let old_offchain_signer_commitment = key_set.leaf_hash();
717        let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
718        let index =
719            Self::insert_or_reuse_authenticator_key(&mut key_set, new_authenticator_pubkey)?;
720        let new_offchain_signer_commitment = key_set.leaf_hash();
721
722        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
723
724        #[allow(clippy::cast_possible_truncation)]
725        // truncating is intentional, and index will always fit in 32 bits
726        let signature = sign_insert_authenticator(
727            &self.signer.onchain_signer(),
728            leaf_index,
729            new_authenticator_address,
730            index as u32,
731            encoded_offchain_pubkey,
732            new_offchain_signer_commitment.into(),
733            nonce,
734            &eip712_domain,
735        )
736        .await
737        .map_err(|e| {
738            AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
739        })?;
740
741        #[allow(clippy::cast_possible_truncation)]
742        // truncating is intentional, and index will always fit in 32 bits
743        let req = InsertAuthenticatorRequest {
744            leaf_index,
745            new_authenticator_address,
746            pubkey_id: index as u32,
747            new_authenticator_pubkey: encoded_offchain_pubkey,
748            old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
749            new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
750            signature: signature.as_bytes().to_vec(),
751            nonce,
752        };
753
754        let resp = self
755            .http_client
756            .post(format!(
757                "{}/insert-authenticator",
758                self.config.gateway_url()
759            ))
760            .json(&req)
761            .send()
762            .await?;
763
764        let status = resp.status();
765        if status.is_success() {
766            let body: GatewayStatusResponse = resp.json().await?;
767            Ok(body.request_id)
768        } else {
769            let body_text = Self::response_body_or_fallback(resp).await;
770            Err(AuthenticatorError::GatewayError {
771                status,
772                body: body_text,
773            })
774        }
775    }
776
777    /// Updates an existing authenticator slot with a new authenticator.
778    ///
779    /// # Errors
780    /// Returns an error if the gateway rejects the request or a network error occurs.
781    ///
782    /// # Note
783    /// TODO: After successfully updating an authenticator, the `packed_account_data` should be
784    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
785    pub async fn update_authenticator(
786        &mut self,
787        old_authenticator_address: Address,
788        new_authenticator_address: Address,
789        new_authenticator_pubkey: EdDSAPublicKey,
790        index: u32,
791    ) -> Result<String, AuthenticatorError> {
792        let leaf_index = self.leaf_index();
793        let nonce = self.signing_nonce().await?;
794        let mut key_set = self.fetch_authenticator_pubkeys().await?;
795        let old_commitment: U256 = key_set.leaf_hash().into();
796        let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
797        key_set.try_set_at_index(index as usize, new_authenticator_pubkey)?;
798        let new_commitment: U256 = key_set.leaf_hash().into();
799
800        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
801
802        let signature = sign_update_authenticator(
803            &self.signer.onchain_signer(),
804            leaf_index,
805            old_authenticator_address,
806            new_authenticator_address,
807            index,
808            encoded_offchain_pubkey,
809            new_commitment,
810            nonce,
811            &eip712_domain,
812        )
813        .await
814        .map_err(|e| {
815            AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
816        })?;
817
818        let req = UpdateAuthenticatorRequest {
819            leaf_index,
820            old_authenticator_address,
821            new_authenticator_address,
822            old_offchain_signer_commitment: old_commitment,
823            new_offchain_signer_commitment: new_commitment,
824            signature: signature.as_bytes().to_vec(),
825            nonce,
826            pubkey_id: index,
827            new_authenticator_pubkey: encoded_offchain_pubkey,
828        };
829
830        let resp = self
831            .http_client
832            .post(format!(
833                "{}/update-authenticator",
834                self.config.gateway_url()
835            ))
836            .json(&req)
837            .send()
838            .await?;
839
840        let status = resp.status();
841        if status.is_success() {
842            let gateway_resp: GatewayStatusResponse = resp.json().await?;
843            Ok(gateway_resp.request_id)
844        } else {
845            let body_text = Self::response_body_or_fallback(resp).await;
846            Err(AuthenticatorError::GatewayError {
847                status,
848                body: body_text,
849            })
850        }
851    }
852
853    /// Removes an authenticator from the account.
854    ///
855    /// # Errors
856    /// Returns an error if the gateway rejects the request or a network error occurs.
857    ///
858    /// # Note
859    /// TODO: After successfully removing an authenticator, the `packed_account_data` should be
860    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
861    pub async fn remove_authenticator(
862        &mut self,
863        authenticator_address: Address,
864        index: u32,
865    ) -> Result<String, AuthenticatorError> {
866        let leaf_index = self.leaf_index();
867        let nonce = self.signing_nonce().await?;
868        let mut key_set = self.fetch_authenticator_pubkeys().await?;
869        let old_commitment: U256 = key_set.leaf_hash().into();
870        let existing_pubkey = key_set
871            .get(index as usize)
872            .ok_or(AuthenticatorError::PublicKeyNotFound)?;
873
874        let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
875
876        key_set.try_clear_at_index(index as usize)?;
877        let new_commitment: U256 = key_set.leaf_hash().into();
878
879        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
880
881        let signature = sign_remove_authenticator(
882            &self.signer.onchain_signer(),
883            leaf_index,
884            authenticator_address,
885            index,
886            encoded_old_offchain_pubkey,
887            new_commitment,
888            nonce,
889            &eip712_domain,
890        )
891        .await
892        .map_err(|e| {
893            AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
894        })?;
895
896        let req = RemoveAuthenticatorRequest {
897            leaf_index,
898            authenticator_address,
899            old_offchain_signer_commitment: old_commitment,
900            new_offchain_signer_commitment: new_commitment,
901            signature: signature.as_bytes().to_vec(),
902            nonce,
903            pubkey_id: Some(index),
904            authenticator_pubkey: Some(encoded_old_offchain_pubkey),
905        };
906
907        let resp = self
908            .http_client
909            .post(format!(
910                "{}/remove-authenticator",
911                self.config.gateway_url()
912            ))
913            .json(&req)
914            .send()
915            .await?;
916
917        let status = resp.status();
918        if status.is_success() {
919            let gateway_resp: GatewayStatusResponse = resp.json().await?;
920            Ok(gateway_resp.request_id)
921        } else {
922            let body_text = Self::response_body_or_fallback(resp).await;
923            Err(AuthenticatorError::GatewayError {
924                status,
925                body: body_text,
926            })
927        }
928    }
929}
930
931/// Represents an account in the process of being initialized,
932/// i.e. it is not yet registered in the `WorldIDRegistry` contract.
933pub struct InitializingAuthenticator {
934    request_id: String,
935    http_client: reqwest::Client,
936    config: Config,
937}
938
939impl InitializingAuthenticator {
940    /// Creates a new World ID account by adding it to the registry using the gateway.
941    ///
942    /// # Errors
943    /// - See `Signer::from_seed_bytes` for additional error details.
944    /// - Will error if the gateway rejects the request or a network error occurs.
945    async fn new(
946        seed: &[u8],
947        config: Config,
948        recovery_address: Option<Address>,
949        http_client: reqwest::Client,
950    ) -> Result<Self, AuthenticatorError> {
951        let signer = Signer::from_seed_bytes(seed)?;
952
953        let mut key_set = AuthenticatorPublicKeySet::default();
954        key_set.try_push(signer.offchain_signer_pubkey())?;
955        let leaf_hash = key_set.leaf_hash();
956
957        let offchain_pubkey_compressed = {
958            let pk = signer.offchain_signer_pubkey().pk;
959            let mut compressed_bytes = Vec::new();
960            pk.serialize_compressed(&mut compressed_bytes)
961                .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
962            U256::from_le_slice(&compressed_bytes)
963        };
964
965        let req = CreateAccountRequest {
966            recovery_address,
967            authenticator_addresses: vec![signer.onchain_signer_address()],
968            authenticator_pubkeys: vec![offchain_pubkey_compressed],
969            offchain_signer_commitment: leaf_hash.into(),
970        };
971
972        let resp = http_client
973            .post(format!("{}/create-account", config.gateway_url()))
974            .json(&req)
975            .send()
976            .await?;
977
978        let status = resp.status();
979        if status.is_success() {
980            let body: GatewayStatusResponse = resp.json().await?;
981            Ok(Self {
982                request_id: body.request_id,
983                http_client,
984                config,
985            })
986        } else {
987            let body_text = Authenticator::response_body_or_fallback(resp).await;
988            Err(AuthenticatorError::GatewayError {
989                status,
990                body: body_text,
991            })
992        }
993    }
994
995    /// Poll the status of the World ID creation request.
996    ///
997    /// # Errors
998    /// - Will error if the network request fails.
999    /// - Will error if the gateway returns an error response.
1000    pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
1001        let resp = self
1002            .http_client
1003            .get(format!(
1004                "{}/status/{}",
1005                self.config.gateway_url(),
1006                self.request_id
1007            ))
1008            .send()
1009            .await?;
1010
1011        let status = resp.status();
1012
1013        if status.is_success() {
1014            let body: GatewayStatusResponse = resp.json().await?;
1015            Ok(body.status)
1016        } else {
1017            let body_text = Authenticator::response_body_or_fallback(resp).await;
1018            Err(AuthenticatorError::GatewayError {
1019                status,
1020                body: body_text,
1021            })
1022        }
1023    }
1024}
1025
1026impl ProtocolSigner for Authenticator {
1027    fn sign(&self, message: FieldElement) -> EdDSASignature {
1028        self.signer
1029            .offchain_signer_private_key()
1030            .expose_secret()
1031            .sign(*message)
1032    }
1033}
1034
1035/// A trait for types that can be represented as a `U256` on-chain.
1036pub trait OnchainKeyRepresentable {
1037    /// Converts an off-chain public key into a `U256` representation for on-chain use in the `WorldIDRegistry` contract.
1038    ///
1039    /// The `U256` representation is a 32-byte little-endian encoding of the **compressed** (single point) public key.
1040    ///
1041    /// # Errors
1042    /// Will error if the public key unexpectedly fails to serialize.
1043    fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
1044}
1045
1046impl OnchainKeyRepresentable for EdDSAPublicKey {
1047    // REVIEW: updating to BE
1048    fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
1049        let mut compressed_bytes = Vec::new();
1050        self.pk
1051            .serialize_compressed(&mut compressed_bytes)
1052            .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1053        Ok(U256::from_le_slice(&compressed_bytes))
1054    }
1055}
1056
1057/// Errors that can occur when interacting with the Authenticator.
1058#[derive(Debug, thiserror::Error)]
1059pub enum AuthenticatorError {
1060    /// Primitive error
1061    #[error(transparent)]
1062    PrimitiveError(#[from] PrimitiveError),
1063
1064    /// This operation requires a registered account and an account is not registered
1065    /// for this authenticator. Call `create_account` first to register it.
1066    #[error("Account is not registered for this authenticator.")]
1067    AccountDoesNotExist,
1068
1069    /// The account already exists for this authenticator. Call `leaf_index` to get the leaf index.
1070    #[error("Account already exists for this authenticator.")]
1071    AccountAlreadyExists,
1072
1073    /// An error occurred while interacting with the EVM contract.
1074    #[error("Error interacting with EVM contract: {0}")]
1075    ContractError(#[from] alloy::contract::Error),
1076
1077    /// Network/HTTP request error.
1078    #[error("Network error: {0}")]
1079    NetworkError(#[from] reqwest::Error),
1080
1081    /// Public key not found in the Authenticator public key set. Usually indicates the local state is out of sync with the registry.
1082    #[error("Public key not found.")]
1083    PublicKeyNotFound,
1084
1085    /// Gateway returned an error response.
1086    #[error("Gateway error (status {status}): {body}")]
1087    GatewayError {
1088        /// HTTP status code
1089        status: StatusCode,
1090        /// Response body
1091        body: String,
1092    },
1093
1094    /// Indexer returned an error response.
1095    #[error("Indexer error (status {status}): {body}")]
1096    IndexerError {
1097        /// HTTP status code
1098        status: StatusCode,
1099        /// Response body
1100        body: String,
1101    },
1102
1103    /// Account creation timed out while polling for confirmation.
1104    #[error("Account creation timed out")]
1105    Timeout,
1106
1107    /// Configuration is invalid or missing required values.
1108    #[error("Invalid configuration for {attribute}: {reason}")]
1109    InvalidConfig {
1110        /// The config attribute that is invalid.
1111        attribute: &'static str,
1112        /// Description of why it is invalid.
1113        reason: String,
1114    },
1115
1116    /// The provided credential is not valid for the provided proof request.
1117    #[error("The provided credential is not valid for the provided proof request")]
1118    InvalidCredentialForProofRequest,
1119
1120    /// Error during the World ID registration process.
1121    ///
1122    /// This usually occurs from an on-chain revert.
1123    #[error("Registration error ({error_code}): {error_message}")]
1124    RegistrationError {
1125        /// Error code from the registration process.
1126        error_code: String,
1127        /// Detailed error message.
1128        error_message: String,
1129    },
1130
1131    /// Error on proof generation
1132    #[error(transparent)]
1133    ProofError(#[from] ProofError),
1134
1135    /// Indexer returned an authenticator key slot that exceeds supported key capacity.
1136    #[error(
1137        "Invalid indexer authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
1138    )]
1139    InvalidIndexerPubkeySlot {
1140        /// Slot index returned by the indexer.
1141        slot_index: usize,
1142        /// Highest supported slot index.
1143        max_supported_slot: usize,
1144    },
1145
1146    /// Generic error for other unexpected issues.
1147    #[error("{0}")]
1148    Generic(String),
1149}
1150
1151#[derive(Debug)]
1152enum PollResult {
1153    Retryable,
1154    TerminalError(AuthenticatorError),
1155}
1156
1157#[cfg(all(test, feature = "embed-zkeys"))]
1158mod tests {
1159    use super::*;
1160    use alloy::primitives::{U256, address};
1161    use std::sync::OnceLock;
1162    use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
1163
1164    fn test_materials() -> (Arc<CircomGroth16Material>, Arc<CircomGroth16Material>) {
1165        static QUERY: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1166        static NULLIFIER: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1167
1168        let query = QUERY.get_or_init(|| {
1169            Arc::new(world_id_proof::proof::load_embedded_query_material().unwrap())
1170        });
1171        let nullifier = NULLIFIER.get_or_init(|| {
1172            Arc::new(world_id_proof::proof::load_embedded_nullifier_material().unwrap())
1173        });
1174
1175        (Arc::clone(query), Arc::clone(nullifier))
1176    }
1177
1178    fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
1179        Signer::from_seed_bytes(&[seed_byte; 32])
1180            .unwrap()
1181            .offchain_signer_pubkey()
1182    }
1183
1184    fn encoded_test_pubkey(seed_byte: u8) -> U256 {
1185        test_pubkey(seed_byte).to_ethereum_representation().unwrap()
1186    }
1187
1188    #[test]
1189    fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
1190        let mut key_set =
1191            AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
1192                .unwrap();
1193        key_set[1] = None;
1194        let new_key = test_pubkey(3);
1195
1196        let index =
1197            Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1198
1199        assert_eq!(index, 1);
1200        assert_eq!(key_set.len(), 3);
1201        assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
1202    }
1203
1204    #[test]
1205    fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
1206        let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
1207        let new_key = test_pubkey(2);
1208
1209        let index =
1210            Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1211
1212        assert_eq!(index, 1);
1213        assert_eq!(key_set.len(), 2);
1214        assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1215    }
1216
1217    #[test]
1218    fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
1219        let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
1220        encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
1221
1222        let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
1223
1224        assert_eq!(key_set.len(), 2);
1225        assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
1226        assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1227    }
1228
1229    #[test]
1230    fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
1231        let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
1232        encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
1233
1234        let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
1235        assert!(matches!(
1236            error,
1237            AuthenticatorError::InvalidIndexerPubkeySlot {
1238                slot_index,
1239                max_supported_slot
1240            } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
1241        ));
1242    }
1243
1244    /// Tests that `get_packed_account_data` correctly fetches the packed account data from the indexer
1245    /// when no RPC is configured.
1246    #[tokio::test]
1247    async fn test_get_packed_account_data_from_indexer() {
1248        let mut server = mockito::Server::new_async().await;
1249        let indexer_url = server.url();
1250
1251        let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1252        let expected_packed_index = U256::from(42);
1253
1254        let mock = server
1255            .mock("POST", "/packed-account")
1256            .match_header("content-type", "application/json")
1257            .match_body(mockito::Matcher::JsonString(
1258                serde_json::json!({
1259                    "authenticator_address": test_address
1260                })
1261                .to_string(),
1262            ))
1263            .with_status(200)
1264            .with_header("content-type", "application/json")
1265            .with_body(
1266                serde_json::json!({
1267                    "packed_account_data": format!("{:#x}", expected_packed_index)
1268                })
1269                .to_string(),
1270            )
1271            .create_async()
1272            .await;
1273
1274        let config = Config::new(
1275            None,
1276            1,
1277            address!("0x0000000000000000000000000000000000000001"),
1278            indexer_url,
1279            "http://gateway.example.com".to_string(),
1280            Vec::new(),
1281            2,
1282        )
1283        .unwrap();
1284
1285        let http_client = reqwest::Client::new();
1286
1287        let result = Authenticator::get_packed_account_data(
1288            test_address,
1289            None, // No registry, force indexer usage
1290            &config,
1291            &http_client,
1292        )
1293        .await
1294        .unwrap();
1295
1296        assert_eq!(result, expected_packed_index);
1297        mock.assert_async().await;
1298        drop(server);
1299    }
1300
1301    #[tokio::test]
1302    async fn test_get_packed_account_data_from_indexer_error() {
1303        let mut server = mockito::Server::new_async().await;
1304        let indexer_url = server.url();
1305
1306        let test_address = address!("0x0000000000000000000000000000000000000099");
1307
1308        let mock = server
1309            .mock("POST", "/packed-account")
1310            .with_status(400)
1311            .with_header("content-type", "application/json")
1312            .with_body(
1313                serde_json::json!({
1314                    "code": "account_does_not_exist",
1315                    "message": "There is no account for this authenticator address"
1316                })
1317                .to_string(),
1318            )
1319            .create_async()
1320            .await;
1321
1322        let config = Config::new(
1323            None,
1324            1,
1325            address!("0x0000000000000000000000000000000000000001"),
1326            indexer_url,
1327            "http://gateway.example.com".to_string(),
1328            Vec::new(),
1329            2,
1330        )
1331        .unwrap();
1332
1333        let http_client = reqwest::Client::new();
1334
1335        let result =
1336            Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1337
1338        assert!(matches!(
1339            result,
1340            Err(AuthenticatorError::AccountDoesNotExist)
1341        ));
1342        mock.assert_async().await;
1343        drop(server);
1344    }
1345
1346    #[tokio::test]
1347    async fn test_signing_nonce_from_indexer() {
1348        let mut server = mockito::Server::new_async().await;
1349        let indexer_url = server.url();
1350
1351        let leaf_index = U256::from(1);
1352        let expected_nonce = U256::from(5);
1353
1354        let mock = server
1355            .mock("POST", "/signature-nonce")
1356            .match_header("content-type", "application/json")
1357            .match_body(mockito::Matcher::JsonString(
1358                serde_json::json!({
1359                    "leaf_index": format!("{:#x}", leaf_index)
1360                })
1361                .to_string(),
1362            ))
1363            .with_status(200)
1364            .with_header("content-type", "application/json")
1365            .with_body(
1366                serde_json::json!({
1367                    "signature_nonce": format!("{:#x}", expected_nonce)
1368                })
1369                .to_string(),
1370            )
1371            .create_async()
1372            .await;
1373
1374        let config = Config::new(
1375            None,
1376            1,
1377            address!("0x0000000000000000000000000000000000000001"),
1378            indexer_url,
1379            "http://gateway.example.com".to_string(),
1380            Vec::new(),
1381            2,
1382        )
1383        .unwrap();
1384
1385        let (query_material, nullifier_material) = test_materials();
1386        let authenticator = Authenticator {
1387            config,
1388            packed_account_data: leaf_index, // This sets leaf_index() to 1
1389            signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1390            registry: None, // No registry - forces indexer usage
1391            http_client: reqwest::Client::new(),
1392            ws_connector: Connector::Plain,
1393            query_material,
1394            nullifier_material,
1395        };
1396
1397        let nonce = authenticator.signing_nonce().await.unwrap();
1398
1399        assert_eq!(nonce, expected_nonce);
1400        mock.assert_async().await;
1401        drop(server);
1402    }
1403
1404    #[tokio::test]
1405    async fn test_signing_nonce_from_indexer_error() {
1406        let mut server = mockito::Server::new_async().await;
1407        let indexer_url = server.url();
1408
1409        let mock = server
1410            .mock("POST", "/signature-nonce")
1411            .with_status(400)
1412            .with_header("content-type", "application/json")
1413            .with_body(
1414                serde_json::json!({
1415                    "code": "invalid_leaf_index",
1416                    "message": "Account index cannot be zero"
1417                })
1418                .to_string(),
1419            )
1420            .create_async()
1421            .await;
1422
1423        let config = Config::new(
1424            None,
1425            1,
1426            address!("0x0000000000000000000000000000000000000001"),
1427            indexer_url,
1428            "http://gateway.example.com".to_string(),
1429            Vec::new(),
1430            2,
1431        )
1432        .unwrap();
1433
1434        let (query_material, nullifier_material) = test_materials();
1435        let authenticator = Authenticator {
1436            config,
1437            packed_account_data: U256::ZERO,
1438            signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1439            registry: None,
1440            http_client: reqwest::Client::new(),
1441            ws_connector: Connector::Plain,
1442            query_material,
1443            nullifier_material,
1444        };
1445
1446        let result = authenticator.signing_nonce().await;
1447
1448        assert!(matches!(
1449            result,
1450            Err(AuthenticatorError::IndexerError { .. })
1451        ));
1452        mock.assert_async().await;
1453        drop(server);
1454    }
1455}