1use crate::{
4 defaults, error::WalletKitError, primitives::ParseFromForeignBinding, Environment,
5 FieldElement, Region,
6};
7use alloy_core::primitives::Address;
8use ruint::aliases::U256;
9use ruint_uniffi::Uint256;
10use std::sync::Arc;
11use world_id_core::{
12 api_types::{GatewayErrorCode, GatewayRequestState},
13 primitives::{AuthenticatorPublicKeySet, Config},
14 Authenticator as CoreAuthenticator, Credential as CoreCredential, CredentialInput,
15 InitializingAuthenticator as CoreInitializingAuthenticator,
16 OnchainKeyRepresentable, Signer,
17};
18
19use crate::requests::{ProofRequest, ProofResponse};
20use crate::storage::CredentialStore;
21#[cfg(not(target_arch = "wasm32"))]
22use crate::storage::StoragePaths;
23use crate::OwnershipProof;
24
25mod with_storage;
26
27#[derive(Clone, uniffi::Object)]
29pub struct Groth16Materials {
30 query: Arc<world_id_core::proof::CircomGroth16Material>,
31 nullifier: Arc<world_id_core::proof::CircomGroth16Material>,
32}
33
34impl std::fmt::Debug for Groth16Materials {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("Groth16Materials").finish_non_exhaustive()
37 }
38}
39
40#[cfg(feature = "embed-zkeys")]
44#[uniffi::export]
45impl Groth16Materials {
46 #[uniffi::constructor]
56 pub fn from_embedded() -> Result<Self, WalletKitError> {
57 let query =
58 world_id_core::proof::load_embedded_query_material().map_err(|error| {
59 WalletKitError::Groth16MaterialEmbeddedLoad {
60 error: error.to_string(),
61 }
62 })?;
63 let nullifier = world_id_core::proof::load_embedded_nullifier_material()
64 .map_err(|error| WalletKitError::Groth16MaterialEmbeddedLoad {
65 error: error.to_string(),
66 })?;
67 Ok(Self {
68 query: Arc::new(query),
69 nullifier: Arc::new(nullifier),
70 })
71 }
72}
73
74#[cfg(not(target_arch = "wasm32"))]
78#[uniffi::export]
79impl Groth16Materials {
80 #[uniffi::constructor]
91 #[expect(
95 clippy::needless_pass_by_value,
96 reason = "UniFFI constructors require owned Arc arguments"
97 )]
98 pub fn from_cache(paths: Arc<StoragePaths>) -> Result<Self, WalletKitError> {
99 let query_zkey = paths.query_zkey_path();
100 let nullifier_zkey = paths.nullifier_zkey_path();
101 let query_graph = paths.query_graph_path();
102 let nullifier_graph = paths.nullifier_graph_path();
103
104 let query = world_id_core::proof::load_query_material_from_paths(
105 &query_zkey,
106 &query_graph,
107 )
108 .map_err(|error| WalletKitError::Groth16MaterialCacheInvalid {
109 path: format!(
110 "{} and {}",
111 query_zkey.to_string_lossy(),
112 query_graph.to_string_lossy()
113 ),
114 error: error.to_string(),
115 })?;
116
117 let nullifier = world_id_core::proof::load_nullifier_material_from_paths(
118 &nullifier_zkey,
119 &nullifier_graph,
120 )
121 .map_err(|error| WalletKitError::Groth16MaterialCacheInvalid {
122 path: format!(
123 "{} and {}",
124 nullifier_zkey.to_string_lossy(),
125 nullifier_graph.to_string_lossy()
126 ),
127 error: error.to_string(),
128 })?;
129
130 Ok(Self {
131 query: Arc::new(query),
132 nullifier: Arc::new(nullifier),
133 })
134 }
135}
136
137#[derive(Debug, uniffi::Object)]
139pub struct Authenticator {
140 inner: CoreAuthenticator,
141 store: Arc<CredentialStore>,
142}
143
144impl Authenticator {
145 pub async fn init_with_config(
151 seed: &[u8],
152 config: Config,
153 materials: Arc<Groth16Materials>,
154 store: Arc<CredentialStore>,
155 ) -> Result<Self, WalletKitError> {
156 let authenticator = CoreAuthenticator::init(seed, config)
157 .await?
158 .with_proof_materials(
159 Arc::clone(&materials.query),
160 Arc::clone(&materials.nullifier),
161 );
162 Ok(Self {
163 inner: authenticator,
164 store,
165 })
166 }
167}
168
169#[uniffi::export(async_runtime = "tokio")]
170impl Authenticator {
171 #[must_use]
176 pub fn packed_account_data(&self) -> Uint256 {
177 self.inner.packed_account_data.into()
178 }
179
180 #[must_use]
185 pub fn leaf_index(&self) -> u64 {
186 self.inner.leaf_index()
187 }
188
189 #[must_use]
193 pub fn onchain_address(&self) -> String {
194 self.inner.onchain_address().to_string()
195 }
196
197 #[tracing::instrument(
202 target = "walletkit_latency",
203 name = "rpc_account_data",
204 skip_all
205 )]
206 pub async fn get_packed_account_data_remote(
207 &self,
208 ) -> Result<Uint256, WalletKitError> {
209 let packed_account_data = self.inner.fetch_packed_account_data().await?;
210 Ok(packed_account_data.into())
211 }
212
213 #[tracing::instrument(
222 target = "walletkit_latency",
223 name = "oprf_blinding_factor",
224 skip_all
225 )]
226 pub async fn generate_credential_blinding_factor_remote(
227 &self,
228 issuer_schema_id: u64,
229 ) -> Result<FieldElement, WalletKitError> {
230 Ok(self
231 .inner
232 .generate_credential_blinding_factor(issuer_schema_id)
233 .await
234 .map(Into::into)?)
235 }
236
237 #[must_use]
239 pub fn compute_credential_sub(
240 &self,
241 blinding_factor: &FieldElement,
242 ) -> FieldElement {
243 CoreCredential::compute_sub(self.inner.leaf_index(), blinding_factor.0).into()
244 }
245
246 pub fn danger_sign_challenge(
257 &self,
258 challenge: &[u8],
259 ) -> Result<Vec<u8>, WalletKitError> {
260 let signature = self.inner.danger_sign_challenge(challenge)?;
261 Ok(signature.as_bytes().to_vec())
262 }
263
264 pub async fn danger_sign_initiate_recovery_agent_update(
289 &self,
290 new_recovery_agent: String,
291 ) -> Result<RecoveryUpdateSignature, WalletKitError> {
292 let new_recovery_agent =
293 Address::parse_from_ffi(&new_recovery_agent, "new_recovery_agent")?;
294 let (sig, nonce) = self
295 .inner
296 .danger_sign_initiate_recovery_agent_update(new_recovery_agent)
297 .await?;
298 Ok(RecoveryUpdateSignature {
299 signature: sig.as_bytes().to_vec(),
300 nonce: nonce.into(),
301 })
302 }
303
304 #[allow(deprecated)]
319 pub async fn initiate_recovery_agent_update(
320 &self,
321 new_recovery_agent: String,
322 ) -> Result<String, WalletKitError> {
323 let new_recovery_agent =
324 Address::parse_from_ffi(&new_recovery_agent, "new_recovery_agent")?;
325
326 let request_id = self
327 .inner
328 .initiate_recovery_agent_update(new_recovery_agent)
329 .await?;
330
331 Ok(request_id.to_string())
332 }
333
334 #[allow(deprecated)]
346 pub async fn execute_recovery_agent_update(
347 &self,
348 ) -> Result<String, WalletKitError> {
349 let request_id = self.inner.execute_recovery_agent_update().await?;
350
351 Ok(request_id.to_string())
352 }
353
354 #[allow(deprecated)]
364 pub async fn cancel_recovery_agent_update(&self) -> Result<String, WalletKitError> {
365 let request_id = self.inner.cancel_recovery_agent_update().await?;
366
367 Ok(request_id.to_string())
368 }
369}
370
371#[uniffi::export(async_runtime = "tokio")]
372impl Authenticator {
373 #[uniffi::constructor]
381 #[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
382 pub async fn init_with_defaults(
383 seed: &[u8],
384 rpc_url: Option<String>,
385 environment: &Environment,
386 region: Option<Region>,
387 materials: Arc<Groth16Materials>,
388 store: Arc<CredentialStore>,
389 ) -> Result<Self, WalletKitError> {
390 let config = defaults::default_config(environment, rpc_url, region)?;
391 Self::init_with_config(seed, config, materials, store).await
392 }
393
394 #[uniffi::constructor]
404 #[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
405 pub async fn init_with_ohttp_defaults(
406 seed: &[u8],
407 rpc_url: Option<String>,
408 environment: &Environment,
409 region: Option<Region>,
410 materials: Arc<Groth16Materials>,
411 store: Arc<CredentialStore>,
412 ) -> Result<Self, WalletKitError> {
413 let config = defaults::default_config_with_ohttp(environment, rpc_url, region)?;
414 Self::init_with_config(seed, config, materials, store).await
415 }
416
417 #[uniffi::constructor]
425 #[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
426 pub async fn init(
427 seed: &[u8],
428 config: &str,
429 materials: Arc<Groth16Materials>,
430 store: Arc<CredentialStore>,
431 ) -> Result<Self, WalletKitError> {
432 let config =
433 Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
434 attribute: "config".to_string(),
435 reason: "Invalid config".to_string(),
436 })?;
437 Self::init_with_config(seed, config, materials, store).await
438 }
439
440 pub async fn generate_proof(
445 &self,
446 proof_request: &ProofRequest,
447 now: Option<u64>,
448 ) -> Result<ProofResponse, WalletKitError> {
449 let now = if let Some(n) = now {
450 n
451 } else {
452 #[cfg(target_arch = "wasm32")]
453 {
454 return Err(WalletKitError::InvalidInput {
455 attribute: "now".to_string(),
456 reason: "`now` must be provided on wasm32 targets".to_string(),
457 });
458 }
459
460 #[cfg(not(target_arch = "wasm32"))]
461 {
462 let start = std::time::SystemTime::now();
463 start
464 .duration_since(std::time::UNIX_EPOCH)
465 .map_err(|e| WalletKitError::Generic {
466 error: format!("Critical. Unable to determine SystemTime: {e}"),
467 })?
468 .as_secs()
469 }
470 };
471
472 let credentials: Vec<_> = self
477 .store
478 .list_credentials(None, now)?
479 .iter()
480 .filter(|c| !c.is_expired)
481 .filter_map(|cred| {
482 if let Ok(Some((credential, blinding_factor))) =
483 self.store.get_credential(cred.issuer_schema_id, now)
484 {
485 Some(CredentialInput {
486 credential: credential.into(),
487 blinding_factor: blinding_factor.into(),
488 })
489 } else {
490 tracing::warn!(
491 issuer_schema_id = %cred.issuer_schema_id,
492 credential_id = %cred.credential_id,
493 "credential listed but not loadable, skipping"
494 );
495 None
496 }
497 })
498 .collect();
499
500 let account_inclusion_proof =
501 self.fetch_inclusion_proof_with_cache(now).await?;
502
503 let nullifier = Box::pin(self.inner.generate_nullifier(
506 &proof_request.0,
507 Some(account_inclusion_proof.clone()),
508 ))
509 .await?;
510
511 if self
512 .store
513 .is_nullifier_replay(nullifier.verifiable_oprf_output.output.into(), now)?
514 {
515 return Err(WalletKitError::NullifierReplay);
516 }
517
518 let session_id_r_seed =
520 proof_request
521 .0
522 .session_id
523 .and_then(|session_id| {
524 match self.store.get_session_seed(session_id.oprf_seed, now) {
525 Ok(seed) => seed,
526 Err(err) => {
527 tracing::warn!(error = %err, "failed to load cached session seed, continuing without");
528 None
529 }
530 }
531 });
532
533 let result = Box::pin(self.inner.generate_proof(
535 &proof_request.0,
536 nullifier.clone(),
537 &credentials,
538 Some(account_inclusion_proof),
539 session_id_r_seed,
540 ))
541 .await?;
542
543 if let Some(seed) = result.session_id_r_seed {
546 if let Some(session_id) = result.proof_response.session_id {
547 if let Err(err) =
548 self.store
549 .store_session_seed(session_id.oprf_seed, seed, now)
550 {
551 tracing::error!("error caching session_id_r_seed: {}", err);
552 }
553 }
554 }
555
556 self.store
557 .replay_guard_set(nullifier.verifiable_oprf_output.output.into(), now)?;
558
559 Ok(result.proof_response.into())
560 }
561
562 pub async fn prove_credential_sub(
587 &self,
588 nonce: &FieldElement,
589 blinding_factor: &FieldElement,
590 sub: &FieldElement,
591 ) -> Result<OwnershipProof, WalletKitError> {
592 #[cfg(target_arch = "wasm32")]
593 {
594 let _ = (nonce, blinding_factor, sub);
595 return Err(WalletKitError::Generic {
596 error: "credential ownership proofs are not supported on wasm32"
597 .to_string(),
598 });
599 }
600
601 #[cfg(not(target_arch = "wasm32"))]
602 {
603 let now = std::time::SystemTime::now()
604 .duration_since(std::time::UNIX_EPOCH)
605 .map_err(|e| WalletKitError::Generic {
606 error: format!("Critical. Unable to determine SystemTime: {e}"),
607 })?
608 .as_secs();
609
610 let inclusion_proof = self.fetch_inclusion_proof_with_cache(now).await?;
611 let proof = self
612 .inner
613 .prove_credential_sub(
614 nonce.0,
615 blinding_factor.0,
616 sub.0,
617 Some(inclusion_proof),
618 )
619 .await?;
620
621 Ok(OwnershipProof(proof))
622 }
623 }
624}
625
626#[derive(Debug, Clone, uniffi::Enum)]
628pub enum RegistrationStatus {
629 Queued,
631 Batching,
633 Submitted,
635 Finalized,
637 Failed {
639 error: String,
641 error_code: Option<String>,
643 },
644}
645
646impl From<GatewayRequestState> for RegistrationStatus {
647 fn from(state: GatewayRequestState) -> Self {
648 match state {
649 GatewayRequestState::Queued => Self::Queued,
650 GatewayRequestState::Batching => Self::Batching,
651 GatewayRequestState::Submitted { .. } => Self::Submitted,
652 GatewayRequestState::Finalized { .. } => Self::Finalized,
653 GatewayRequestState::Failed { error, error_code } => Self::Failed {
654 error,
655 error_code: error_code.map(|c: GatewayErrorCode| c.to_string()),
656 },
657 }
658 }
659}
660
661#[derive(uniffi::Object)]
666pub struct InitializingAuthenticator(CoreInitializingAuthenticator);
667
668#[uniffi::export(async_runtime = "tokio")]
669impl InitializingAuthenticator {
670 #[uniffi::constructor]
678 #[tracing::instrument(
679 target = "walletkit_latency",
680 name = "gateway_register",
681 skip_all
682 )]
683 pub async fn register_with_defaults(
684 seed: &[u8],
685 rpc_url: Option<String>,
686 environment: &Environment,
687 region: Option<Region>,
688 recovery_address: Option<String>,
689 ) -> Result<Self, WalletKitError> {
690 let recovery_address =
691 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
692
693 let config = defaults::default_config(environment, rpc_url, region)?;
694
695 let initializing_authenticator =
696 CoreAuthenticator::register(seed, config, recovery_address).await?;
697
698 Ok(Self(initializing_authenticator))
699 }
700
701 #[uniffi::constructor]
711 #[tracing::instrument(
712 target = "walletkit_latency",
713 name = "gateway_register",
714 skip_all
715 )]
716 pub async fn register_with_ohttp_defaults(
717 seed: &[u8],
718 rpc_url: Option<String>,
719 environment: &Environment,
720 region: Option<Region>,
721 recovery_address: Option<String>,
722 ) -> Result<Self, WalletKitError> {
723 let recovery_address =
724 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
725
726 let config = defaults::default_config_with_ohttp(environment, rpc_url, region)?;
727
728 let initializing_authenticator =
729 CoreAuthenticator::register(seed, config, recovery_address).await?;
730
731 Ok(Self(initializing_authenticator))
732 }
733
734 #[uniffi::constructor]
742 #[tracing::instrument(
743 target = "walletkit_latency",
744 name = "gateway_register",
745 skip_all
746 )]
747 pub async fn register(
748 seed: &[u8],
749 config: &str,
750 recovery_address: Option<String>,
751 ) -> Result<Self, WalletKitError> {
752 let recovery_address =
753 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
754
755 let config =
756 Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
757 attribute: "config".to_string(),
758 reason: "Invalid config".to_string(),
759 })?;
760
761 let initializing_authenticator =
762 CoreAuthenticator::register(seed, config, recovery_address).await?;
763
764 Ok(Self(initializing_authenticator))
765 }
766
767 #[tracing::instrument(
772 target = "walletkit_latency",
773 name = "gateway_poll",
774 skip_all
775 )]
776 pub async fn poll_status(&self) -> Result<RegistrationStatus, WalletKitError> {
777 let status = self.0.poll_status().await?;
778 Ok(status.into())
779 }
780}
781
782#[derive(Debug, Clone, uniffi::Record)]
788pub struct RecoveryUpdateSignature {
789 pub signature: Vec<u8>,
792 pub nonce: Uint256,
795}
796
797#[derive(Debug, Clone, uniffi::Record)]
805pub struct RecoveryData {
806 pub authenticator_address: String,
808 pub authenticator_pubkey: String,
810 pub offchain_signer_commitment: String,
812}
813
814impl RecoveryData {
815 pub fn from_seed(seed: &[u8]) -> Result<Self, WalletKitError> {
824 let signer = Signer::from_seed_bytes(seed)?;
825 let authenticator_address = signer.onchain_signer_address().to_checksum(None);
826 let authenticator_pubkey: U256 = signer
827 .offchain_signer_pubkey()
828 .to_ethereum_representation()?;
829 let mut key_set = AuthenticatorPublicKeySet::default();
830 key_set.try_push(signer.offchain_signer_pubkey())?;
831 let offchain_signer_commitment: U256 = key_set.leaf_hash().into();
832
833 Ok(Self {
834 authenticator_address,
835 authenticator_pubkey: format!("{authenticator_pubkey:#066x}"),
836 offchain_signer_commitment: format!("{offchain_signer_commitment:#066x}"),
837 })
838 }
839}
840
841#[uniffi::export]
848pub fn recovery_data_from_seed(seed: &[u8]) -> Result<RecoveryData, WalletKitError> {
849 RecoveryData::from_seed(seed)
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 #[test]
857 fn test_recovery_data_from_seed() {
858 let seed = [1u8; 32];
859 let material = RecoveryData::from_seed(&seed).expect("should derive material");
860
861 assert!(material.authenticator_address.starts_with("0x"));
862 assert_eq!(material.authenticator_address.len(), 42);
863 assert!(material.authenticator_pubkey.starts_with("0x"));
864 assert!(material.authenticator_pubkey.len() <= 66);
865 assert!(material.offchain_signer_commitment.starts_with("0x"));
866 assert!(material.offchain_signer_commitment.len() <= 66);
867 assert!(material.authenticator_address.len() > 2);
868 assert!(material.authenticator_pubkey.len() > 2);
869 assert!(material.offchain_signer_commitment.len() > 2);
870 }
871
872 #[test]
873 fn test_recovery_data_rejects_invalid_seed() {
874 assert!(RecoveryData::from_seed(&[0u8; 16]).is_err());
875 assert!(RecoveryData::from_seed(&[]).is_err());
876 }
877
878 #[cfg(feature = "embed-zkeys")]
879 #[tokio::test]
880 async fn test_init_with_config_and_materials() {
881 use crate::storage::tests_utils::{
882 cleanup_test_storage, temp_root_path, InMemoryStorageProvider,
883 };
884 use alloy::primitives::address;
885 use world_id_core::primitives::{Config, ServiceEndpoint};
886
887 let _ = rustls::crypto::ring::default_provider().install_default();
888
889 let mut mock_server = mockito::Server::new_async().await;
890 mock_server
891 .mock("POST", "/")
892 .with_status(200)
893 .with_header("content-type", "application/json")
894 .with_body(
895 serde_json::json!({
896 "jsonrpc": "2.0",
897 "id": 1,
898 "result": "0x0000000000000000000000000000000000000000000000000000000000000001"
899 })
900 .to_string(),
901 )
902 .create_async()
903 .await;
904
905 let config = Config::new(
906 Some(mock_server.url()),
907 480,
908 address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"),
909 ServiceEndpoint::direct(
910 "https://indexer.us.id-infra.worldcoin.dev".to_string(),
911 ),
912 ServiceEndpoint::direct(
913 "https://gateway.id-infra.worldcoin.dev".to_string(),
914 ),
915 vec![],
916 2,
917 )
918 .unwrap();
919 let config = serde_json::to_string(&config).unwrap();
920
921 let root = temp_root_path();
922 let provider = InMemoryStorageProvider::new(&root);
923 let store = CredentialStore::from_provider(&provider).expect("store");
924 store.init(42, 100).expect("init storage");
925
926 let materials =
927 Arc::new(Groth16Materials::from_embedded().expect("load materials"));
928 let _authenticator =
929 Authenticator::init(&[2u8; 32], &config, materials, Arc::new(store))
930 .await
931 .unwrap();
932 drop(mock_server);
933
934 cleanup_test_storage(&root);
935 }
936}