1use crate::{
4 authenticator::artifacts::WalletKitZkArtifactSource, defaults,
5 error::WalletKitError, primitives::ParseFromForeignBinding, Environment,
6 FieldElement, Region,
7};
8use alloy_core::primitives::Address;
9use ruint::aliases::U256;
10use ruint_uniffi::Uint256;
11use std::sync::Arc;
12use world_id_core::{
13 api_types::{GatewayErrorCode, GatewayRequestState},
14 primitives::{AuthenticatorPublicKeySet, Config},
15 Authenticator as CoreAuthenticator, Credential as CoreCredential, CredentialInput,
16 InitializingAuthenticator as CoreInitializingAuthenticator,
17 OnchainKeyRepresentable, Signer,
18};
19
20use crate::requests::{ProofRequest, ProofResponse};
21use crate::storage::CredentialStore;
22use crate::OwnershipProof;
23
24pub mod artifacts;
25mod with_storage;
26
27#[derive(Debug, uniffi::Object)]
29pub struct Authenticator {
30 inner: CoreAuthenticator,
31 store: Arc<CredentialStore>,
32}
33
34impl Authenticator {
35 pub async fn init_with_config(
41 seed: &[u8],
42 config: Config,
43 artifacts: Arc<dyn WalletKitZkArtifactSource>,
44 store: Arc<CredentialStore>,
45 ) -> Result<Self, WalletKitError> {
46 let authenticator = CoreAuthenticator::init(seed, config, artifacts).await?;
47
48 Ok(Self {
49 inner: authenticator,
50 store,
51 })
52 }
53}
54
55#[uniffi::export(async_runtime = "tokio")]
56impl Authenticator {
57 #[must_use]
62 pub fn packed_account_data(&self) -> Uint256 {
63 self.inner.packed_account_data.into()
64 }
65
66 #[must_use]
71 pub fn leaf_index(&self) -> u64 {
72 self.inner.leaf_index()
73 }
74
75 #[must_use]
79 pub fn onchain_address(&self) -> String {
80 self.inner.onchain_address().to_string()
81 }
82
83 #[tracing::instrument(
88 target = "walletkit_latency",
89 name = "rpc_account_data",
90 skip_all
91 )]
92 pub async fn get_packed_account_data_remote(
93 &self,
94 ) -> Result<Uint256, WalletKitError> {
95 let packed_account_data = self.inner.fetch_packed_account_data().await?;
96 Ok(packed_account_data.into())
97 }
98
99 #[tracing::instrument(
108 target = "walletkit_latency",
109 name = "oprf_blinding_factor",
110 skip_all
111 )]
112 pub async fn generate_credential_blinding_factor_remote(
113 &self,
114 issuer_schema_id: u64,
115 ) -> Result<FieldElement, WalletKitError> {
116 Ok(self
117 .inner
118 .generate_credential_blinding_factor(issuer_schema_id)
119 .await
120 .map(Into::into)?)
121 }
122
123 #[must_use]
125 pub fn compute_credential_sub(
126 &self,
127 blinding_factor: &FieldElement,
128 ) -> FieldElement {
129 CoreCredential::compute_sub(self.inner.leaf_index(), blinding_factor.0).into()
130 }
131
132 pub fn danger_sign_challenge(
143 &self,
144 challenge: &[u8],
145 ) -> Result<Vec<u8>, WalletKitError> {
146 let signature = self.inner.danger_sign_challenge(challenge)?;
147 Ok(signature.as_bytes().to_vec())
148 }
149
150 pub async fn danger_sign_initiate_recovery_agent_update(
174 &self,
175 new_recovery_agent: String,
176 ) -> Result<RecoveryUpdateSignature, WalletKitError> {
177 let new_recovery_agent =
178 Address::parse_from_ffi(&new_recovery_agent, "new_recovery_agent")?;
179 let (sig, nonce) = self
180 .inner
181 .danger_sign_initiate_recovery_agent_update(new_recovery_agent)
182 .await?;
183 Ok(RecoveryUpdateSignature {
184 signature: sig.as_bytes().to_vec(),
185 nonce: nonce.into(),
186 })
187 }
188
189 pub async fn update_recovery_agent(
207 &self,
208 new_recovery_agent: String,
209 ) -> Result<String, WalletKitError> {
210 let new_recovery_agent =
211 Address::parse_from_ffi(&new_recovery_agent, "new_recovery_agent")?;
212
213 let request_id = self.inner.update_recovery_agent(new_recovery_agent).await?;
214
215 Ok(request_id.to_string())
216 }
217
218 pub async fn revert_recovery_agent_update(&self) -> Result<String, WalletKitError> {
232 let request_id = self.inner.revert_recovery_agent_update().await?;
233
234 Ok(request_id.to_string())
235 }
236}
237
238#[uniffi::export(async_runtime = "tokio")]
239impl Authenticator {
240 #[uniffi::constructor]
248 #[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
249 pub async fn init_with_defaults(
250 seed: &[u8],
251 rpc_url: Option<String>,
252 environment: &Environment,
253 region: Option<Region>,
254 artifacts: Arc<dyn WalletKitZkArtifactSource>,
255 store: Arc<CredentialStore>,
256 ) -> Result<Self, WalletKitError> {
257 let config = defaults::default_config(environment, rpc_url, region)?;
258 Self::init_with_config(seed, config, artifacts, store).await
259 }
260
261 #[uniffi::constructor]
271 #[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
272 pub async fn init_with_ohttp_defaults(
273 seed: &[u8],
274 rpc_url: Option<String>,
275 environment: &Environment,
276 region: Option<Region>,
277 artifacts: Arc<dyn WalletKitZkArtifactSource>,
278 store: Arc<CredentialStore>,
279 ) -> Result<Self, WalletKitError> {
280 let config = defaults::default_config_with_ohttp(environment, rpc_url, region)?;
281 Self::init_with_config(seed, config, artifacts, store).await
282 }
283
284 #[uniffi::constructor]
292 #[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
293 pub async fn init(
294 seed: &[u8],
295 config: &str,
296 artifacts: Arc<dyn WalletKitZkArtifactSource>,
297 store: Arc<CredentialStore>,
298 ) -> Result<Self, WalletKitError> {
299 let config =
300 Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
301 attribute: "config".to_string(),
302 reason: "Invalid config".to_string(),
303 })?;
304 Self::init_with_config(seed, config, artifacts, store).await
305 }
306
307 pub async fn generate_proof(
312 &self,
313 proof_request: &ProofRequest,
314 now: Option<u64>,
315 ) -> Result<ProofResponse, WalletKitError> {
316 let now = if let Some(n) = now {
317 n
318 } else {
319 #[cfg(target_arch = "wasm32")]
320 {
321 return Err(WalletKitError::InvalidInput {
322 attribute: "now".to_string(),
323 reason: "`now` must be provided on wasm32 targets".to_string(),
324 });
325 }
326
327 #[cfg(not(target_arch = "wasm32"))]
328 {
329 let start = std::time::SystemTime::now();
330 start
331 .duration_since(std::time::UNIX_EPOCH)
332 .map_err(|e| WalletKitError::Generic {
333 error: format!("Critical. Unable to determine SystemTime: {e}"),
334 })?
335 .as_secs()
336 }
337 };
338
339 let credentials: Vec<_> = self
344 .store
345 .list_credentials(None, now)?
346 .iter()
347 .filter(|c| !c.is_expired)
348 .filter_map(|cred| {
349 if let Ok(Some((credential, blinding_factor))) =
350 self.store.get_credential(cred.issuer_schema_id, now)
351 {
352 Some(CredentialInput {
353 credential: credential.into(),
354 blinding_factor: blinding_factor.into(),
355 })
356 } else {
357 tracing::warn!(
358 issuer_schema_id = %cred.issuer_schema_id,
359 credential_id = %cred.credential_id,
360 "credential listed but not loadable, skipping"
361 );
362 None
363 }
364 })
365 .collect();
366
367 let account_inclusion_proof =
368 self.fetch_inclusion_proof_with_cache(now).await?;
369
370 let nullifier = Box::pin(self.inner.generate_nullifier(
373 &proof_request.0,
374 Some(account_inclusion_proof.clone()),
375 ))
376 .await?;
377
378 if self
379 .store
380 .is_nullifier_replay(nullifier.verifiable_oprf_output.output.into(), now)?
381 {
382 return Err(WalletKitError::NullifierReplay);
383 }
384
385 let session_id_r_seed =
387 proof_request
388 .0
389 .session_id
390 .and_then(|session_id| {
391 match self.store.get_session_seed(session_id.oprf_seed, now) {
392 Ok(seed) => seed,
393 Err(err) => {
394 tracing::warn!(error = %err, "failed to load cached session seed, continuing without");
395 None
396 }
397 }
398 });
399
400 let result = Box::pin(self.inner.generate_proof(
402 &proof_request.0,
403 nullifier.clone(),
404 &credentials,
405 Some(account_inclusion_proof),
406 session_id_r_seed,
407 ))
408 .await?;
409
410 if let Some(seed) = result.session_id_r_seed {
413 if let Some(session_id) = result.proof_response.session_id {
414 if let Err(err) =
415 self.store
416 .store_session_seed(session_id.oprf_seed, seed, now)
417 {
418 tracing::error!("error caching session_id_r_seed: {}", err);
419 }
420 }
421 }
422
423 self.store
424 .replay_guard_set(nullifier.verifiable_oprf_output.output.into(), now)?;
425
426 Ok(result.proof_response.into())
427 }
428
429 pub async fn prove_credential_sub(
454 &self,
455 nonce: &FieldElement,
456 blinding_factor: &FieldElement,
457 sub: &FieldElement,
458 ) -> Result<OwnershipProof, WalletKitError> {
459 #[cfg(target_arch = "wasm32")]
460 {
461 let _ = (nonce, blinding_factor, sub);
462 return Err(WalletKitError::Generic {
463 error: "credential ownership proofs are not supported on wasm32"
464 .to_string(),
465 });
466 }
467
468 #[cfg(not(target_arch = "wasm32"))]
469 {
470 let now = std::time::SystemTime::now()
471 .duration_since(std::time::UNIX_EPOCH)
472 .map_err(|e| WalletKitError::Generic {
473 error: format!("Critical. Unable to determine SystemTime: {e}"),
474 })?
475 .as_secs();
476
477 let inclusion_proof = self.fetch_inclusion_proof_with_cache(now).await?;
478 let proof = self
479 .inner
480 .prove_credential_sub(
481 nonce.0,
482 blinding_factor.0,
483 sub.0,
484 Some(inclusion_proof),
485 )
486 .await?;
487
488 Ok(OwnershipProof(proof))
489 }
490 }
491}
492
493#[derive(Debug, Clone, uniffi::Enum)]
495pub enum RegistrationStatus {
496 Queued,
498 Batching,
500 Submitted,
502 Finalized,
504 Failed {
506 error: String,
508 error_code: Option<String>,
510 },
511}
512
513impl From<GatewayRequestState> for RegistrationStatus {
514 fn from(state: GatewayRequestState) -> Self {
515 match state {
516 GatewayRequestState::Queued => Self::Queued,
517 GatewayRequestState::Batching => Self::Batching,
518 GatewayRequestState::Submitted { .. } => Self::Submitted,
519 GatewayRequestState::Finalized { .. } => Self::Finalized,
520 GatewayRequestState::Failed { error, error_code } => Self::Failed {
521 error,
522 error_code: error_code.map(|c: GatewayErrorCode| c.to_string()),
523 },
524 }
525 }
526}
527
528#[derive(uniffi::Object)]
533pub struct InitializingAuthenticator(CoreInitializingAuthenticator);
534
535#[uniffi::export(async_runtime = "tokio")]
536impl InitializingAuthenticator {
537 #[uniffi::constructor]
545 #[tracing::instrument(
546 target = "walletkit_latency",
547 name = "gateway_register",
548 skip_all
549 )]
550 pub async fn register_with_defaults(
551 seed: &[u8],
552 rpc_url: Option<String>,
553 environment: &Environment,
554 region: Option<Region>,
555 recovery_address: Option<String>,
556 ) -> Result<Self, WalletKitError> {
557 let recovery_address =
558 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
559
560 let config = defaults::default_config(environment, rpc_url, region)?;
561
562 let initializing_authenticator =
563 CoreAuthenticator::register(seed, config, recovery_address).await?;
564
565 Ok(Self(initializing_authenticator))
566 }
567
568 #[uniffi::constructor]
578 #[tracing::instrument(
579 target = "walletkit_latency",
580 name = "gateway_register",
581 skip_all
582 )]
583 pub async fn register_with_ohttp_defaults(
584 seed: &[u8],
585 rpc_url: Option<String>,
586 environment: &Environment,
587 region: Option<Region>,
588 recovery_address: Option<String>,
589 ) -> Result<Self, WalletKitError> {
590 let recovery_address =
591 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
592
593 let config = defaults::default_config_with_ohttp(environment, rpc_url, region)?;
594
595 let initializing_authenticator =
596 CoreAuthenticator::register(seed, config, recovery_address).await?;
597
598 Ok(Self(initializing_authenticator))
599 }
600
601 #[uniffi::constructor]
609 #[tracing::instrument(
610 target = "walletkit_latency",
611 name = "gateway_register",
612 skip_all
613 )]
614 pub async fn register(
615 seed: &[u8],
616 config: &str,
617 recovery_address: Option<String>,
618 ) -> Result<Self, WalletKitError> {
619 let recovery_address =
620 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
621
622 let config =
623 Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
624 attribute: "config".to_string(),
625 reason: "Invalid config".to_string(),
626 })?;
627
628 let initializing_authenticator =
629 CoreAuthenticator::register(seed, config, recovery_address).await?;
630
631 Ok(Self(initializing_authenticator))
632 }
633
634 #[tracing::instrument(
639 target = "walletkit_latency",
640 name = "gateway_poll",
641 skip_all
642 )]
643 pub async fn poll_status(&self) -> Result<RegistrationStatus, WalletKitError> {
644 let status = self.0.poll_status().await?;
645 Ok(status.into())
646 }
647}
648
649#[derive(Debug, Clone, uniffi::Record)]
655pub struct RecoveryUpdateSignature {
656 pub signature: Vec<u8>,
659 pub nonce: Uint256,
662}
663
664#[derive(Debug, Clone, uniffi::Record)]
672pub struct RecoveryData {
673 pub authenticator_address: String,
675 pub authenticator_pubkey: String,
677 pub offchain_signer_commitment: String,
679}
680
681impl RecoveryData {
682 pub fn from_seed(seed: &[u8]) -> Result<Self, WalletKitError> {
691 let signer = Signer::from_seed_bytes(seed)?;
692 let authenticator_address = signer.onchain_signer_address().to_checksum(None);
693 let authenticator_pubkey: U256 = signer
694 .offchain_signer_pubkey()
695 .to_ethereum_representation()?;
696 let mut key_set = AuthenticatorPublicKeySet::default();
697 key_set.try_push(signer.offchain_signer_pubkey())?;
698 let offchain_signer_commitment: U256 = key_set.leaf_hash().into();
699
700 Ok(Self {
701 authenticator_address,
702 authenticator_pubkey: format!("{authenticator_pubkey:#066x}"),
703 offchain_signer_commitment: format!("{offchain_signer_commitment:#066x}"),
704 })
705 }
706}
707
708#[uniffi::export]
715pub fn recovery_data_from_seed(seed: &[u8]) -> Result<RecoveryData, WalletKitError> {
716 RecoveryData::from_seed(seed)
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn test_recovery_data_from_seed() {
725 let seed = [1u8; 32];
726 let material = RecoveryData::from_seed(&seed).expect("should derive material");
727
728 assert!(material.authenticator_address.starts_with("0x"));
729 assert_eq!(material.authenticator_address.len(), 42);
730 assert!(material.authenticator_pubkey.starts_with("0x"));
731 assert!(material.authenticator_pubkey.len() <= 66);
732 assert!(material.offchain_signer_commitment.starts_with("0x"));
733 assert!(material.offchain_signer_commitment.len() <= 66);
734 assert!(material.authenticator_address.len() > 2);
735 assert!(material.authenticator_pubkey.len() > 2);
736 assert!(material.offchain_signer_commitment.len() > 2);
737 }
738
739 #[test]
740 fn test_recovery_data_rejects_invalid_seed() {
741 assert!(RecoveryData::from_seed(&[0u8; 16]).is_err());
742 assert!(RecoveryData::from_seed(&[]).is_err());
743 }
744
745 #[cfg(feature = "embed-zkeys")]
746 #[tokio::test]
747 async fn test_init_with_config_and_materials() {
748 use crate::{
749 authenticator::artifacts::caching::CachingZkArtifacts,
750 storage::tests_utils::{
751 cleanup_test_storage, temp_root_path, InMemoryStorageProvider,
752 },
753 };
754 use alloy::primitives::address;
755 use world_id_core::primitives::{Config, ServiceEndpoint};
756
757 let _ = rustls::crypto::ring::default_provider().install_default();
758
759 let mut mock_server = mockito::Server::new_async().await;
760 mock_server
761 .mock("POST", "/")
762 .with_status(200)
763 .with_header("content-type", "application/json")
764 .with_body(
765 serde_json::json!({
766 "jsonrpc": "2.0",
767 "id": 1,
768 "result": "0x0000000000000000000000000000000000000000000000000000000000000001"
769 })
770 .to_string(),
771 )
772 .create_async()
773 .await;
774
775 let config = Config::new(
776 Some(mock_server.url()),
777 480,
778 address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"),
779 ServiceEndpoint::direct(
780 "https://indexer.us.id-infra.worldcoin.dev".to_string(),
781 ),
782 ServiceEndpoint::direct(
783 "https://gateway.id-infra.worldcoin.dev".to_string(),
784 ),
785 vec![],
786 2,
787 )
788 .unwrap();
789 let config = serde_json::to_string(&config).unwrap();
790
791 let root = temp_root_path();
792 let provider = InMemoryStorageProvider::new(&root);
793 let store = CredentialStore::from_provider(&provider).expect("store");
794 store.init(42, 100).expect("init storage");
795
796 let artifacts =
797 Arc::new(CachingZkArtifacts::new(Arc::new(store.paths().unwrap())));
798
799 let _authenticator =
800 Authenticator::init(&[2u8; 32], &config, artifacts, Arc::new(store))
801 .await
802 .unwrap();
803 drop(mock_server);
804
805 cleanup_test_storage(&root);
806 }
807}