1use std::sync::Arc;
6use std::time::Duration;
7
8use crate::requests::ProofRequest;
9use crate::types::{
10 AccountInclusionProof, CreateAccountRequest, GatewayRequestState, GatewayStatusResponse,
11 IndexerErrorCode, IndexerPackedAccountRequest, IndexerPackedAccountResponse,
12 IndexerQueryRequest, IndexerSignatureNonceResponse, InsertAuthenticatorRequest,
13 RemoveAuthenticatorRequest, ServiceApiError, UpdateAuthenticatorRequest,
14};
15use crate::world_id_registry::WorldIdRegistry::{self, WorldIdRegistryInstance};
16use crate::world_id_registry::{
17 domain, sign_insert_authenticator, sign_remove_authenticator, sign_update_authenticator,
18};
19use crate::{Credential, FieldElement, Signer};
20use alloy::primitives::{Address, U256};
21use alloy::providers::{DynProvider, Provider, ProviderBuilder};
22use alloy::uint;
23use ark_babyjubjub::EdwardsAffine;
24use ark_bn254::Bn254;
25use ark_serialize::CanonicalSerialize;
26use backon::{ExponentialBuilder, Retryable};
27use circom_types::groth16::Proof;
28use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
29use reqwest::StatusCode;
30use rustls::{ClientConfig, RootCertStore};
31use secrecy::ExposeSecret;
32use taceo_oprf_client::Connector;
33use taceo_oprf_types::ShareEpoch;
34use world_id_primitives::authenticator::AuthenticatorPublicKeySet;
35use world_id_primitives::merkle::MerkleInclusionProof;
36use world_id_primitives::proof::SingleProofInput;
37use world_id_primitives::PrimitiveError;
38pub use world_id_primitives::{authenticator::ProtocolSigner, Config, TREE_DEPTH};
39
40static MASK_RECOVERY_COUNTER: U256 =
41 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
42static MASK_PUBKEY_ID: U256 =
43 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
44static MASK_LEAF_INDEX: U256 =
45 uint!(0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_U256);
46
47type UniquenessProof = (Proof<Bn254>, FieldElement);
48
49pub struct Authenticator {
51 pub config: Config,
53 pub packed_account_data: U256,
56 signer: Signer,
57 registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
58 http_client: reqwest::Client,
59 ws_connector: Connector,
60}
61
62#[expect(clippy::missing_fields_in_debug)]
63impl std::fmt::Debug for Authenticator {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("Authenticator")
66 .field("config", &self.config)
67 .field("packed_account_data", &self.packed_account_data)
68 .field("signer", &self.signer)
69 .finish()
70 }
71}
72
73impl Authenticator {
74 pub async fn init(seed: &[u8], config: Config) -> Result<Self, AuthenticatorError> {
84 let signer = Signer::from_seed_bytes(seed)?;
85
86 let registry = config.rpc_url().map_or_else(
87 || None,
88 |rpc_url| {
89 let provider = ProviderBuilder::new()
90 .with_chain_id(config.chain_id())
91 .connect_http(rpc_url.clone());
92 Some(WorldIdRegistry::new(
93 *config.registry_address(),
94 provider.erased(),
95 ))
96 },
97 );
98
99 let http_client = reqwest::Client::new();
100
101 let packed_account_data = Self::get_packed_account_data(
102 signer.onchain_signer_address(),
103 registry.as_ref(),
104 &config,
105 &http_client,
106 )
107 .await?;
108
109 let mut root_store = RootCertStore::empty();
110 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
111 let rustls_config = ClientConfig::builder()
112 .with_root_certificates(root_store)
113 .with_no_client_auth();
114 let ws_connector = Connector::Rustls(Arc::new(rustls_config));
115
116 Ok(Self {
117 packed_account_data,
118 signer,
119 config,
120 registry: registry.map(Arc::new),
121 http_client,
122 ws_connector,
123 })
124 }
125
126 pub async fn register(
134 seed: &[u8],
135 config: Config,
136 recovery_address: Option<Address>,
137 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
138 let http_client = reqwest::Client::new();
139 InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
140 }
141
142 pub async fn init_or_register(
155 seed: &[u8],
156 config: Config,
157 recovery_address: Option<Address>,
158 ) -> Result<Self, AuthenticatorError> {
159 match Self::init(seed, config.clone()).await {
160 Ok(authenticator) => Ok(authenticator),
161 Err(AuthenticatorError::AccountDoesNotExist) => {
162 let http_client = reqwest::Client::new();
164 let initializing_authenticator = InitializingAuthenticator::new(
165 seed,
166 config.clone(),
167 recovery_address,
168 http_client,
169 )
170 .await?;
171
172 let backoff = ExponentialBuilder::default()
173 .with_min_delay(Duration::from_millis(800))
174 .with_factor(1.5)
175 .without_max_times()
176 .with_total_delay(Some(Duration::from_secs(120)));
177
178 let poller = || async {
179 let poll_status = initializing_authenticator.poll_status().await;
180 let result = match poll_status {
181 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
182 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
183 PollResult::TerminalError(AuthenticatorError::RegistrationError {
184 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
185 error_message: error,
186 }),
187 ),
188 Err(AuthenticatorError::GatewayError { status, body }) => {
189 if status.is_client_error() {
190 Err(PollResult::TerminalError(
191 AuthenticatorError::GatewayError { status, body },
192 ))
193 } else {
194 Err(PollResult::Retryable)
195 }
196 }
197 _ => Err(PollResult::Retryable),
198 };
199
200 match result {
201 Ok(()) => match Self::init(seed, config.clone()).await {
202 Ok(auth) => Ok(auth),
203 Err(AuthenticatorError::AccountDoesNotExist) => {
204 Err(PollResult::Retryable)
205 }
206 Err(e) => Err(PollResult::TerminalError(e)),
207 },
208 Err(e) => Err(e),
209 }
210 };
211
212 let result = poller
213 .retry(backoff)
214 .when(|e| matches!(e, PollResult::Retryable))
215 .await;
216
217 match result {
218 Ok(authenticator) => Ok(authenticator),
219 Err(PollResult::TerminalError(e)) => Err(e),
220 Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
221 }
222 }
223 Err(e) => Err(e),
224 }
225 }
226
227 pub async fn get_packed_account_data(
235 onchain_signer_address: Address,
236 registry: Option<&WorldIdRegistryInstance<DynProvider>>,
237 config: &Config,
238 http_client: &reqwest::Client,
239 ) -> Result<U256, AuthenticatorError> {
240 let raw_index = if let Some(registry) = registry {
242 registry
243 .authenticatorAddressToPackedAccountData(onchain_signer_address)
244 .call()
245 .await?
246 } else {
247 let url = format!("{}/packed-account", config.indexer_url());
248 let req = IndexerPackedAccountRequest {
249 authenticator_address: onchain_signer_address,
250 };
251 let resp = http_client.post(&url).json(&req).send().await?;
252
253 let status = resp.status();
254 if !status.is_success() {
255 if let Ok(error_resp) = resp.json::<ServiceApiError<IndexerErrorCode>>().await {
257 return match error_resp.code {
258 IndexerErrorCode::AccountDoesNotExist => {
259 Err(AuthenticatorError::AccountDoesNotExist)
260 }
261 _ => Err(AuthenticatorError::IndexerError {
262 status,
263 body: error_resp.message,
264 }),
265 };
266 }
267 return Err(AuthenticatorError::IndexerError {
268 status,
269 body: "Failed to parse indexer error response".to_string(),
270 });
271 }
272
273 let response: IndexerPackedAccountResponse = resp.json().await?;
274 response.packed_account_data
275 };
276
277 if raw_index == U256::ZERO {
278 return Err(AuthenticatorError::AccountDoesNotExist);
279 }
280
281 Ok(raw_index)
282 }
283
284 #[must_use]
287 pub const fn onchain_address(&self) -> Address {
288 self.signer.onchain_signer_address()
289 }
290
291 #[must_use]
294 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
295 self.signer.offchain_signer_pubkey()
296 }
297
298 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
303 let pk = self.signer.offchain_signer_pubkey().pk;
304 let mut compressed_bytes = Vec::new();
305 pk.serialize_compressed(&mut compressed_bytes)
306 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
307 Ok(U256::from_le_slice(&compressed_bytes))
308 }
309
310 #[must_use]
312 pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
313 self.registry.clone()
314 }
315
316 #[must_use]
320 pub fn leaf_index(&self) -> U256 {
321 self.packed_account_data & MASK_LEAF_INDEX
322 }
323
324 #[must_use]
328 pub fn recovery_counter(&self) -> U256 {
329 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
330 recovery_counter >> 224
331 }
332
333 #[must_use]
337 pub fn pubkey_id(&self) -> U256 {
338 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
339 pubkey_id >> 192
340 }
341
342 pub async fn fetch_inclusion_proof(
348 &self,
349 ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
350 {
351 let url = format!("{}/inclusion-proof", self.config.indexer_url());
352 let req = IndexerQueryRequest {
353 leaf_index: self.leaf_index(),
354 };
355 let response = self.http_client.post(&url).json(&req).send().await?;
356 let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
357
358 Ok((response.inclusion_proof, response.authenticator_pubkeys))
359 }
360
361 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
366 let registry = self.registry();
367 if let Some(registry) = registry {
368 let nonce = registry
369 .leafIndexToSignatureNonce(self.leaf_index())
370 .call()
371 .await?;
372 Ok(nonce)
373 } else {
374 let url = format!("{}/signature-nonce", self.config.indexer_url());
375 let req = IndexerQueryRequest {
376 leaf_index: self.leaf_index(),
377 };
378 let resp = self.http_client.post(&url).json(&req).send().await?;
379
380 let status = resp.status();
381 if !status.is_success() {
382 return Err(AuthenticatorError::IndexerError {
383 status,
384 body: resp
385 .json()
386 .await
387 .unwrap_or_else(|_| "Unable to parse response".to_string()),
388 });
389 }
390
391 let response: IndexerSignatureNonceResponse = resp.json().await?;
392 Ok(response.signature_nonce)
393 }
394 }
395
396 #[allow(clippy::future_not_send)]
406 pub async fn generate_proof(
407 &self,
408 proof_request: ProofRequest,
409 credential: Credential,
410 credential_sub_blinding_factor: FieldElement,
411 ) -> Result<UniquenessProof, AuthenticatorError> {
412 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
413 let key_index = key_set
414 .iter()
415 .position(|pk| pk.pk == self.offchain_pubkey().pk)
416 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
417
418 let query_material = crate::proof::load_embedded_query_material();
420 let nullifier_material = crate::proof::load_embedded_nullifier_material();
421
422 let request_item = proof_request
423 .find_request_by_issuer_schema_id(credential.issuer_schema_id.into())
424 .ok_or(AuthenticatorError::InvalidCredentialForProofRequest)?;
425
426 let mut rng = rand::thread_rng();
427
428 let session_id_r_seed = FieldElement::random(&mut rng);
429
430 let args = SingleProofInput::<TREE_DEPTH> {
431 credential,
432 inclusion_proof,
433 key_set,
434 key_index,
435 session_id_r_seed,
436 credential_sub_blinding_factor,
437 rp_id: proof_request.rp_id,
438 share_epoch: ShareEpoch::default().into_inner(),
439 action: proof_request.action,
440 nonce: proof_request.nonce,
441 current_timestamp: proof_request.created_at,
442 rp_signature: proof_request.signature,
443 oprf_public_key: proof_request.oprf_public_key,
444 signal_hash: request_item.signal_hash(),
445 genesis_issued_at_min: request_item.genesis_issued_at_min.unwrap_or(0), };
447
448 let private_key = self.signer.offchain_signer_private_key().expose_secret();
449
450 let services = self.config.nullifier_oracle_urls();
451 if services.is_empty() {
452 return Err(AuthenticatorError::Generic(
453 "No nullifier oracle URLs configured".to_string(),
454 ));
455 }
456 let requested_threshold = self.config.nullifier_oracle_threshold();
457 if requested_threshold == 0 {
458 return Err(AuthenticatorError::InvalidConfig {
459 attribute: "nullifier_oracle_threshold",
460 reason: "must be at least 1".to_string(),
461 });
462 }
463 let threshold = requested_threshold.min(services.len());
464
465 let mut rng = rand::thread_rng();
466 let (proof, _public, nullifier, _id_commitment) = crate::proof::nullifier(
467 services,
468 threshold,
469 &query_material,
470 &nullifier_material,
471 args,
472 private_key,
473 self.ws_connector.clone(),
474 &mut rng,
475 )
476 .await
477 .map_err(|e| AuthenticatorError::Generic(format!("Failed to generate nullifier: {e}")))?;
478
479 Ok((proof, nullifier.into()))
480 }
481
482 pub async fn insert_authenticator(
491 &mut self,
492 new_authenticator_pubkey: EdDSAPublicKey,
493 new_authenticator_address: Address,
494 ) -> Result<String, AuthenticatorError> {
495 let leaf_index = self.leaf_index();
496 let nonce = self.signing_nonce().await?;
497 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
498 let old_offchain_signer_commitment = key_set.leaf_hash();
499 key_set.try_push(new_authenticator_pubkey.clone())?;
500 let index = key_set.len() - 1;
501 let new_offchain_signer_commitment = key_set.leaf_hash();
502
503 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
504
505 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
506
507 #[allow(clippy::cast_possible_truncation)]
508 let signature = sign_insert_authenticator(
510 &self.signer.onchain_signer(),
511 leaf_index,
512 new_authenticator_address,
513 index as u32,
514 encoded_offchain_pubkey,
515 new_offchain_signer_commitment.into(),
516 nonce,
517 &eip712_domain,
518 )
519 .await
520 .map_err(|e| {
521 AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
522 })?;
523
524 #[allow(clippy::cast_possible_truncation)]
525 let req = InsertAuthenticatorRequest {
527 leaf_index,
528 new_authenticator_address,
529 pubkey_id: index as u32,
530 new_authenticator_pubkey: encoded_offchain_pubkey,
531 old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
532 new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
533 sibling_nodes: inclusion_proof
534 .siblings
535 .iter()
536 .map(|s| (*s).into())
537 .collect(),
538 signature: signature.as_bytes().to_vec(),
539 nonce,
540 };
541
542 let resp = self
543 .http_client
544 .post(format!(
545 "{}/insert-authenticator",
546 self.config.gateway_url()
547 ))
548 .json(&req)
549 .send()
550 .await?;
551
552 let status = resp.status();
553 if status.is_success() {
554 let body: GatewayStatusResponse = resp.json().await?;
555 Ok(body.request_id)
556 } else {
557 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
558 Err(AuthenticatorError::GatewayError {
559 status,
560 body: body_text,
561 })
562 }
563 }
564
565 pub async fn update_authenticator(
574 &mut self,
575 old_authenticator_address: Address,
576 new_authenticator_address: Address,
577 new_authenticator_pubkey: EdDSAPublicKey,
578 index: u32,
579 ) -> Result<String, AuthenticatorError> {
580 let leaf_index = self.leaf_index();
581 let nonce = self.signing_nonce().await?;
582 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
583 let old_commitment: U256 = key_set.leaf_hash().into();
584 key_set.try_set_at_index(index as usize, new_authenticator_pubkey.clone())?;
585 let new_commitment: U256 = key_set.leaf_hash().into();
586
587 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
588
589 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
590
591 let signature = sign_update_authenticator(
592 &self.signer.onchain_signer(),
593 leaf_index,
594 old_authenticator_address,
595 new_authenticator_address,
596 index,
597 encoded_offchain_pubkey,
598 new_commitment,
599 nonce,
600 &eip712_domain,
601 )
602 .await
603 .map_err(|e| {
604 AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
605 })?;
606
607 let sibling_nodes: Vec<U256> = inclusion_proof
608 .siblings
609 .iter()
610 .map(|s| (*s).into())
611 .collect();
612
613 let req = UpdateAuthenticatorRequest {
614 leaf_index,
615 old_authenticator_address,
616 new_authenticator_address,
617 old_offchain_signer_commitment: old_commitment,
618 new_offchain_signer_commitment: new_commitment,
619 sibling_nodes,
620 signature: signature.as_bytes().to_vec(),
621 nonce,
622 pubkey_id: index,
623 new_authenticator_pubkey: encoded_offchain_pubkey,
624 };
625
626 let resp = self
627 .http_client
628 .post(format!(
629 "{}/update-authenticator",
630 self.config.gateway_url()
631 ))
632 .json(&req)
633 .send()
634 .await?;
635
636 let status = resp.status();
637 if status.is_success() {
638 let gateway_resp: GatewayStatusResponse = resp.json().await?;
639 Ok(gateway_resp.request_id)
640 } else {
641 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
642 Err(AuthenticatorError::GatewayError {
643 status,
644 body: body_text,
645 })
646 }
647 }
648
649 pub async fn remove_authenticator(
658 &mut self,
659 authenticator_address: Address,
660 index: u32,
661 ) -> Result<String, AuthenticatorError> {
662 let leaf_index = self.leaf_index();
663 let nonce = self.signing_nonce().await?;
664 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
665 let old_commitment: U256 = key_set.leaf_hash().into();
666 let existing_pubkey = key_set
667 .get(index as usize)
668 .ok_or(AuthenticatorError::PublicKeyNotFound)?;
669
670 let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
671
672 key_set[index as usize] = EdDSAPublicKey {
673 pk: EdwardsAffine::default(),
674 };
675 let new_commitment: U256 = key_set.leaf_hash().into();
676
677 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
678
679 let signature = sign_remove_authenticator(
680 &self.signer.onchain_signer(),
681 leaf_index,
682 authenticator_address,
683 index,
684 encoded_old_offchain_pubkey,
685 new_commitment,
686 nonce,
687 &eip712_domain,
688 )
689 .await
690 .map_err(|e| {
691 AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
692 })?;
693
694 let sibling_nodes: Vec<U256> = inclusion_proof
695 .siblings
696 .iter()
697 .map(|s| (*s).into())
698 .collect();
699
700 let req = RemoveAuthenticatorRequest {
701 leaf_index,
702 authenticator_address,
703 old_offchain_signer_commitment: old_commitment,
704 new_offchain_signer_commitment: new_commitment,
705 sibling_nodes,
706 signature: signature.as_bytes().to_vec(),
707 nonce,
708 pubkey_id: Some(index),
709 authenticator_pubkey: Some(encoded_old_offchain_pubkey),
710 };
711
712 let resp = self
713 .http_client
714 .post(format!(
715 "{}/remove-authenticator",
716 self.config.gateway_url()
717 ))
718 .json(&req)
719 .send()
720 .await?;
721
722 let status = resp.status();
723 if status.is_success() {
724 let gateway_resp: GatewayStatusResponse = resp.json().await?;
725 Ok(gateway_resp.request_id)
726 } else {
727 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
728 Err(AuthenticatorError::GatewayError {
729 status,
730 body: body_text,
731 })
732 }
733 }
734}
735
736pub struct InitializingAuthenticator {
739 request_id: String,
740 http_client: reqwest::Client,
741 config: Config,
742}
743
744impl InitializingAuthenticator {
745 async fn new(
751 seed: &[u8],
752 config: Config,
753 recovery_address: Option<Address>,
754 http_client: reqwest::Client,
755 ) -> Result<Self, AuthenticatorError> {
756 let signer = Signer::from_seed_bytes(seed)?;
757
758 let mut key_set = AuthenticatorPublicKeySet::new(None)?;
759 key_set.try_push(signer.offchain_signer_pubkey())?;
760 let leaf_hash = key_set.leaf_hash();
761
762 let offchain_pubkey_compressed = {
763 let pk = signer.offchain_signer_pubkey().pk;
764 let mut compressed_bytes = Vec::new();
765 pk.serialize_compressed(&mut compressed_bytes)
766 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
767 U256::from_le_slice(&compressed_bytes)
768 };
769
770 let req = CreateAccountRequest {
771 recovery_address,
772 authenticator_addresses: vec![signer.onchain_signer_address()],
773 authenticator_pubkeys: vec![offchain_pubkey_compressed],
774 offchain_signer_commitment: leaf_hash.into(),
775 };
776
777 let resp = http_client
778 .post(format!("{}/create-account", config.gateway_url()))
779 .json(&req)
780 .send()
781 .await?;
782
783 let status = resp.status();
784 if status.is_success() {
785 let body: GatewayStatusResponse = resp.json().await?;
786 Ok(Self {
787 request_id: body.request_id,
788 http_client,
789 config,
790 })
791 } else {
792 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
793 Err(AuthenticatorError::GatewayError {
794 status,
795 body: body_text,
796 })
797 }
798 }
799
800 pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
806 let resp = self
807 .http_client
808 .get(format!(
809 "{}/status/{}",
810 self.config.gateway_url(),
811 self.request_id
812 ))
813 .send()
814 .await?;
815
816 let status = resp.status();
817
818 if status.is_success() {
819 let body: GatewayStatusResponse = resp.json().await?;
820 Ok(body.status)
821 } else {
822 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
823 Err(AuthenticatorError::GatewayError {
824 status,
825 body: body_text,
826 })
827 }
828 }
829}
830
831impl ProtocolSigner for Authenticator {
832 fn sign(&self, message: FieldElement) -> EdDSASignature {
833 self.signer
834 .offchain_signer_private_key()
835 .expose_secret()
836 .sign(*message)
837 }
838}
839
840pub trait OnchainKeyRepresentable {
842 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
849}
850
851impl OnchainKeyRepresentable for EdDSAPublicKey {
852 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
854 let mut compressed_bytes = Vec::new();
855 self.pk
856 .serialize_compressed(&mut compressed_bytes)
857 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
858 Ok(U256::from_le_slice(&compressed_bytes))
859 }
860}
861
862#[derive(Debug, thiserror::Error)]
864pub enum AuthenticatorError {
865 #[error(transparent)]
867 PrimitiveError(#[from] PrimitiveError),
868
869 #[error("Account is not registered for this authenticator.")]
872 AccountDoesNotExist,
873
874 #[error("Account already exists for this authenticator.")]
876 AccountAlreadyExists,
877
878 #[error("Error interacting with EVM contract: {0}")]
880 ContractError(#[from] alloy::contract::Error),
881
882 #[error("Network error: {0}")]
884 NetworkError(#[from] reqwest::Error),
885
886 #[error("Public key not found.")]
888 PublicKeyNotFound,
889
890 #[error("Gateway error (status {status}): {body}")]
892 GatewayError {
893 status: StatusCode,
895 body: String,
897 },
898
899 #[error("Indexer error (status {status}): {body}")]
901 IndexerError {
902 status: StatusCode,
904 body: String,
906 },
907
908 #[error("Account creation timed out")]
910 Timeout,
911
912 #[error("Invalid configuration for {attribute}: {reason}")]
914 InvalidConfig {
915 attribute: &'static str,
917 reason: String,
919 },
920
921 #[error("The provided credential is not valid for the provided proof request")]
923 InvalidCredentialForProofRequest,
924
925 #[error("Registration error ({error_code}): {error_message}")]
929 RegistrationError {
930 error_code: String,
932 error_message: String,
934 },
935
936 #[error("{0}")]
938 Generic(String),
939}
940
941#[derive(Debug)]
942enum PollResult {
943 Retryable,
944 TerminalError(AuthenticatorError),
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950 use alloy::primitives::{address, U256};
951
952 #[tokio::test]
955 async fn test_get_packed_account_data_from_indexer() {
956 let mut server = mockito::Server::new_async().await;
957 let indexer_url = server.url();
958
959 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
960 let expected_packed_index = U256::from(42);
961
962 let mock = server
963 .mock("POST", "/packed-account")
964 .match_header("content-type", "application/json")
965 .match_body(mockito::Matcher::JsonString(
966 serde_json::json!({
967 "authenticator_address": test_address
968 })
969 .to_string(),
970 ))
971 .with_status(200)
972 .with_header("content-type", "application/json")
973 .with_body(
974 serde_json::json!({
975 "packed_account_data": format!("{:#x}", expected_packed_index)
976 })
977 .to_string(),
978 )
979 .create_async()
980 .await;
981
982 let config = Config::new(
983 None,
984 1,
985 address!("0x0000000000000000000000000000000000000001"),
986 indexer_url,
987 "http://gateway.example.com".to_string(),
988 Vec::new(),
989 2,
990 )
991 .unwrap();
992
993 let http_client = reqwest::Client::new();
994
995 let result = Authenticator::get_packed_account_data(
996 test_address,
997 None, &config,
999 &http_client,
1000 )
1001 .await
1002 .unwrap();
1003
1004 assert_eq!(result, expected_packed_index);
1005 mock.assert_async().await;
1006 drop(server);
1007 }
1008
1009 #[tokio::test]
1010 async fn test_get_packed_account_data_from_indexer_error() {
1011 let mut server = mockito::Server::new_async().await;
1012 let indexer_url = server.url();
1013
1014 let test_address = address!("0x0000000000000000000000000000000000000099");
1015
1016 let mock = server
1017 .mock("POST", "/packed-account")
1018 .with_status(400)
1019 .with_header("content-type", "application/json")
1020 .with_body(
1021 serde_json::json!({
1022 "code": "account_does_not_exist",
1023 "message": "There is no account for this authenticator address"
1024 })
1025 .to_string(),
1026 )
1027 .create_async()
1028 .await;
1029
1030 let config = Config::new(
1031 None,
1032 1,
1033 address!("0x0000000000000000000000000000000000000001"),
1034 indexer_url,
1035 "http://gateway.example.com".to_string(),
1036 Vec::new(),
1037 2,
1038 )
1039 .unwrap();
1040
1041 let http_client = reqwest::Client::new();
1042
1043 let result =
1044 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1045
1046 assert!(matches!(
1047 result,
1048 Err(AuthenticatorError::AccountDoesNotExist)
1049 ));
1050 mock.assert_async().await;
1051 drop(server);
1052 }
1053
1054 #[tokio::test]
1055 async fn test_signing_nonce_from_indexer() {
1056 let mut server = mockito::Server::new_async().await;
1057 let indexer_url = server.url();
1058
1059 let leaf_index = U256::from(1);
1060 let expected_nonce = U256::from(5);
1061
1062 let mock = server
1063 .mock("POST", "/signature-nonce")
1064 .match_header("content-type", "application/json")
1065 .match_body(mockito::Matcher::JsonString(
1066 serde_json::json!({
1067 "leaf_index": format!("{:#x}", leaf_index)
1068 })
1069 .to_string(),
1070 ))
1071 .with_status(200)
1072 .with_header("content-type", "application/json")
1073 .with_body(
1074 serde_json::json!({
1075 "signature_nonce": format!("{:#x}", expected_nonce)
1076 })
1077 .to_string(),
1078 )
1079 .create_async()
1080 .await;
1081
1082 let config = Config::new(
1083 None,
1084 1,
1085 address!("0x0000000000000000000000000000000000000001"),
1086 indexer_url,
1087 "http://gateway.example.com".to_string(),
1088 Vec::new(),
1089 2,
1090 )
1091 .unwrap();
1092
1093 let authenticator = Authenticator {
1094 config,
1095 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1097 registry: None, http_client: reqwest::Client::new(),
1099 ws_connector: Connector::Plain,
1100 };
1101
1102 let nonce = authenticator.signing_nonce().await.unwrap();
1103
1104 assert_eq!(nonce, expected_nonce);
1105 mock.assert_async().await;
1106 drop(server);
1107 }
1108
1109 #[tokio::test]
1110 async fn test_signing_nonce_from_indexer_error() {
1111 let mut server = mockito::Server::new_async().await;
1112 let indexer_url = server.url();
1113
1114 let mock = server
1115 .mock("POST", "/signature-nonce")
1116 .with_status(400)
1117 .with_header("content-type", "application/json")
1118 .with_body(
1119 serde_json::json!({
1120 "code": "invalid_leaf_index",
1121 "message": "Account index cannot be zero"
1122 })
1123 .to_string(),
1124 )
1125 .create_async()
1126 .await;
1127
1128 let config = Config::new(
1129 None,
1130 1,
1131 address!("0x0000000000000000000000000000000000000001"),
1132 indexer_url,
1133 "http://gateway.example.com".to_string(),
1134 Vec::new(),
1135 2,
1136 )
1137 .unwrap();
1138
1139 let authenticator = Authenticator {
1140 config,
1141 packed_account_data: U256::ZERO,
1142 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1143 registry: None,
1144 http_client: reqwest::Client::new(),
1145 ws_connector: Connector::Plain,
1146 };
1147
1148 let result = authenticator.signing_nonce().await;
1149
1150 assert!(matches!(
1151 result,
1152 Err(AuthenticatorError::IndexerError { .. })
1153 ));
1154 mock.assert_async().await;
1155 drop(server);
1156 }
1157}