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