1use alloy_primitives::Address;
4use rand::rngs::OsRng;
5use std::sync::Arc;
6use world_id_core::{
7 api_types::{GatewayErrorCode, GatewayRequestState},
8 primitives::Config,
9 requests::{ProofResponse as CoreProofResponse, ResponseItem},
10 Authenticator as CoreAuthenticator, Credential as CoreCredential,
11 FieldElement as CoreFieldElement,
12 InitializingAuthenticator as CoreInitializingAuthenticator,
13};
14
15#[cfg(feature = "storage")]
16use crate::storage::{CredentialStore, StoragePaths};
17use crate::{
18 defaults::DefaultConfig,
19 error::WalletKitError,
20 primitives::ParseFromForeignBinding,
21 requests::{ProofRequest, ProofResponse},
22 Environment, FieldElement, Region, U256Wrapper,
23};
24
25#[cfg(feature = "storage")]
26mod with_storage;
27
28type Groth16Materials = (
29 Arc<world_id_core::proof::CircomGroth16Material>,
30 Arc<world_id_core::proof::CircomGroth16Material>,
31);
32
33#[cfg(not(feature = "storage"))]
34fn load_embedded_materials() -> Result<Groth16Materials, WalletKitError> {
39 let query_material =
40 world_id_core::proof::load_embedded_query_material().map_err(|error| {
41 WalletKitError::Groth16MaterialEmbeddedLoad {
42 error: error.to_string(),
43 }
44 })?;
45 let nullifier_material = world_id_core::proof::load_embedded_nullifier_material()
46 .map_err(|error| {
47 WalletKitError::Groth16MaterialEmbeddedLoad {
48 error: error.to_string(),
49 }
50 })?;
51
52 Ok((Arc::new(query_material), Arc::new(nullifier_material)))
53}
54
55#[cfg(feature = "storage")]
56fn load_cached_materials(
61 paths: &StoragePaths,
62) -> Result<Groth16Materials, WalletKitError> {
63 let query_zkey = paths.query_zkey_path();
64 let nullifier_zkey = paths.nullifier_zkey_path();
65 let query_graph = paths.query_graph_path();
66 let nullifier_graph = paths.nullifier_graph_path();
67
68 let query_material = load_query_material_from_cache(&query_zkey, &query_graph)?;
69 let nullifier_material =
70 load_nullifier_material_from_cache(&nullifier_zkey, &nullifier_graph)?;
71
72 Ok((Arc::new(query_material), Arc::new(nullifier_material)))
73}
74
75#[cfg(feature = "storage")]
76fn load_query_material_from_cache(
81 query_zkey: &std::path::Path,
82 query_graph: &std::path::Path,
83) -> Result<world_id_core::proof::CircomGroth16Material, WalletKitError> {
84 world_id_core::proof::load_query_material_from_paths(query_zkey, query_graph)
85 .map_err(|error| WalletKitError::Groth16MaterialCacheInvalid {
86 path: format!(
87 "{} and {}",
88 query_zkey.to_string_lossy(),
89 query_graph.to_string_lossy()
90 ),
91 error: error.to_string(),
92 })
93}
94
95#[cfg(feature = "storage")]
96#[expect(
97 clippy::unnecessary_wraps,
98 reason = "Temporary wrapper until world-id-core returns Result for nullifier path loader"
99)]
100fn load_nullifier_material_from_cache(
106 nullifier_zkey: &std::path::Path,
107 nullifier_graph: &std::path::Path,
108) -> Result<world_id_core::proof::CircomGroth16Material, WalletKitError> {
109 Ok(world_id_core::proof::load_nullifier_material_from_paths(
112 nullifier_zkey,
113 nullifier_graph,
114 ))
115}
116
117#[derive(Debug, uniffi::Object)]
119pub struct Authenticator {
120 inner: CoreAuthenticator,
121 #[cfg(feature = "storage")]
122 store: Arc<CredentialStore>,
123}
124
125#[uniffi::export(async_runtime = "tokio")]
126impl Authenticator {
127 #[must_use]
132 pub fn packed_account_data(&self) -> U256Wrapper {
133 self.inner.packed_account_data.into()
134 }
135
136 #[must_use]
141 pub fn leaf_index(&self) -> u64 {
142 self.inner.leaf_index()
143 }
144
145 #[must_use]
149 pub fn onchain_address(&self) -> String {
150 self.inner.onchain_address().to_string()
151 }
152
153 pub async fn get_packed_account_data_remote(
158 &self,
159 ) -> Result<U256Wrapper, WalletKitError> {
160 let client = reqwest::Client::new(); let packed_account_data = CoreAuthenticator::get_packed_account_data(
162 self.inner.onchain_address(),
163 self.inner.registry().as_deref(),
164 &self.inner.config,
165 &client,
166 )
167 .await?;
168 Ok(packed_account_data.into())
169 }
170
171 pub async fn generate_credential_blinding_factor_remote(
180 &self,
181 issuer_schema_id: u64,
182 ) -> Result<FieldElement, WalletKitError> {
183 Ok(self
184 .inner
185 .generate_credential_blinding_factor(issuer_schema_id)
186 .await
187 .map(Into::into)?)
188 }
189
190 #[must_use]
192 pub fn compute_credential_sub(
193 &self,
194 blinding_factor: &FieldElement,
195 ) -> FieldElement {
196 CoreCredential::compute_sub(self.inner.leaf_index(), blinding_factor.0).into()
197 }
198}
199
200#[cfg(not(feature = "storage"))]
201#[uniffi::export(async_runtime = "tokio")]
202impl Authenticator {
203 #[uniffi::constructor]
211 pub async fn init_with_defaults(
212 seed: &[u8],
213 rpc_url: Option<String>,
214 environment: &Environment,
215 region: Option<Region>,
216 ) -> Result<Self, WalletKitError> {
217 let config = Config::from_environment(environment, rpc_url, region)?;
218 let (query_material, nullifier_material) = load_embedded_materials()?;
219 let authenticator =
220 CoreAuthenticator::init(seed, config, query_material, nullifier_material)
221 .await?;
222 Ok(Self {
223 inner: authenticator,
224 })
225 }
226
227 #[uniffi::constructor]
235 pub async fn init(seed: &[u8], config: &str) -> Result<Self, WalletKitError> {
236 let config =
237 Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
238 attribute: "config".to_string(),
239 reason: "Invalid config".to_string(),
240 })?;
241 let (query_material, nullifier_material) = load_embedded_materials()?;
242 let authenticator =
243 CoreAuthenticator::init(seed, config, query_material, nullifier_material)
244 .await?;
245 Ok(Self {
246 inner: authenticator,
247 })
248 }
249}
250
251#[cfg(feature = "storage")]
252#[uniffi::export(async_runtime = "tokio")]
253impl Authenticator {
254 #[uniffi::constructor]
262 pub async fn init_with_defaults(
263 seed: &[u8],
264 rpc_url: Option<String>,
265 environment: &Environment,
266 region: Option<Region>,
267 paths: Arc<StoragePaths>,
268 store: Arc<CredentialStore>,
269 ) -> Result<Self, WalletKitError> {
270 let config = Config::from_environment(environment, rpc_url, region)?;
271 let (query_material, nullifier_material) =
272 load_cached_materials(paths.as_ref())?;
273 let authenticator =
274 CoreAuthenticator::init(seed, config, query_material, nullifier_material)
275 .await?;
276 Ok(Self {
277 inner: authenticator,
278 store,
279 })
280 }
281
282 #[uniffi::constructor]
290 pub async fn init(
291 seed: &[u8],
292 config: &str,
293 paths: Arc<StoragePaths>,
294 store: Arc<CredentialStore>,
295 ) -> Result<Self, WalletKitError> {
296 let config =
297 Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
298 attribute: "config".to_string(),
299 reason: "Invalid config".to_string(),
300 })?;
301 let (query_material, nullifier_material) =
302 load_cached_materials(paths.as_ref())?;
303 let authenticator =
304 CoreAuthenticator::init(seed, config, query_material, nullifier_material)
305 .await?;
306 Ok(Self {
307 inner: authenticator,
308 store,
309 })
310 }
311
312 pub async fn generate_proof(
317 &self,
318 proof_request: &ProofRequest,
319 now: Option<u64>,
320 ) -> Result<ProofResponse, WalletKitError> {
321 let now = if let Some(n) = now {
322 n
323 } else {
324 let start = std::time::SystemTime::now();
325 start
326 .duration_since(std::time::UNIX_EPOCH)
327 .map_err(|e| WalletKitError::Generic {
328 error: format!("Critical. Unable to determine SystemTime: {e}"),
329 })?
330 .as_secs()
331 };
332
333 let credential_list = self.store.list_credentials(None, now)?;
335 let credential_list = credential_list
336 .into_iter()
337 .map(|cred| cred.issuer_schema_id)
338 .collect::<std::collections::HashSet<_>>();
339 let credentials_to_prove = proof_request
340 .0
341 .credentials_to_prove(&credential_list)
342 .ok_or(WalletKitError::UnfulfillableRequest)?;
343
344 let (inclusion_proof, key_set) =
345 self.fetch_inclusion_proof_with_cache(now).await?;
346
347 let nullifier = self
349 .inner
350 .generate_nullifier(&proof_request.0, inclusion_proof, key_set)
351 .await?;
352
353 if self
356 .store
357 .is_nullifier_replay(nullifier.verifiable_oprf_output.output.into(), now)?
358 {
359 return Err(WalletKitError::NullifierReplay);
360 }
361
362 let mut responses: Vec<ResponseItem> = vec![];
363
364 for request_item in credentials_to_prove {
365 let (credential, blinding_factor) = self
366 .store
367 .get_credential(request_item.issuer_schema_id, now)?
368 .ok_or(WalletKitError::CredentialNotIssued)?;
369
370 let session_id_r_seed = CoreFieldElement::random(&mut OsRng); let response_item = self.inner.generate_single_proof(
373 nullifier.clone(),
374 request_item,
375 &credential,
376 blinding_factor.0,
377 session_id_r_seed,
378 proof_request.0.session_id,
379 proof_request.0.created_at,
380 )?;
381 responses.push(response_item);
382 }
383
384 let response = CoreProofResponse {
385 id: proof_request.0.id.clone(),
386 version: world_id_core::requests::RequestVersion::V1,
387 responses,
388 error: None,
389 session_id: None, };
391
392 proof_request
393 .0
394 .validate_response(&response)
395 .map_err(|err| WalletKitError::ResponseValidation(err.to_string()))?;
396
397 self.store
398 .replay_guard_set(nullifier.verifiable_oprf_output.output.into(), now)?;
399
400 Ok(response.into())
401 }
402}
403
404#[derive(Debug, Clone, uniffi::Enum)]
406pub enum RegistrationStatus {
407 Queued,
409 Batching,
411 Submitted,
413 Finalized,
415 Failed {
417 error: String,
419 error_code: Option<String>,
421 },
422}
423
424impl From<GatewayRequestState> for RegistrationStatus {
425 fn from(state: GatewayRequestState) -> Self {
426 match state {
427 GatewayRequestState::Queued => Self::Queued,
428 GatewayRequestState::Batching => Self::Batching,
429 GatewayRequestState::Submitted { .. } => Self::Submitted,
430 GatewayRequestState::Finalized { .. } => Self::Finalized,
431 GatewayRequestState::Failed { error, error_code } => Self::Failed {
432 error,
433 error_code: error_code.map(|c: GatewayErrorCode| c.to_string()),
434 },
435 }
436 }
437}
438
439#[derive(uniffi::Object)]
444pub struct InitializingAuthenticator(CoreInitializingAuthenticator);
445
446#[uniffi::export(async_runtime = "tokio")]
447impl InitializingAuthenticator {
448 #[uniffi::constructor]
456 pub async fn register_with_defaults(
457 seed: &[u8],
458 rpc_url: Option<String>,
459 environment: &Environment,
460 region: Option<Region>,
461 recovery_address: Option<String>,
462 ) -> Result<Self, WalletKitError> {
463 let recovery_address =
464 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
465
466 let config = Config::from_environment(environment, rpc_url, region)?;
467
468 let initializing_authenticator =
469 CoreAuthenticator::register(seed, config, recovery_address).await?;
470
471 Ok(Self(initializing_authenticator))
472 }
473
474 #[uniffi::constructor]
482 pub async fn register(
483 seed: &[u8],
484 config: &str,
485 recovery_address: Option<String>,
486 ) -> Result<Self, WalletKitError> {
487 let recovery_address =
488 Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
489
490 let config =
491 Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
492 attribute: "config".to_string(),
493 reason: "Invalid config".to_string(),
494 })?;
495
496 let initializing_authenticator =
497 CoreAuthenticator::register(seed, config, recovery_address).await?;
498
499 Ok(Self(initializing_authenticator))
500 }
501
502 pub async fn poll_status(&self) -> Result<RegistrationStatus, WalletKitError> {
507 let status = self.0.poll_status().await?;
508 Ok(status.into())
509 }
510}
511
512#[cfg(all(test, feature = "storage"))]
513mod tests {
514 use super::*;
515 use crate::storage::cache_embedded_groth16_material;
516 use crate::storage::tests_utils::{
517 cleanup_test_storage, temp_root_path, InMemoryStorageProvider,
518 };
519 use alloy::primitives::address;
520
521 #[tokio::test]
522 async fn test_init_with_config_and_storage() {
523 let _ = rustls::crypto::ring::default_provider().install_default();
525
526 let mut mock_server = mockito::Server::new_async().await;
527
528 mock_server
530 .mock("POST", "/")
531 .with_status(200)
532 .with_header("content-type", "application/json")
533 .with_body(
534 serde_json::json!({
535 "jsonrpc": "2.0",
536 "id": 1,
537 "result": "0x0000000000000000000000000000000000000000000000000000000000000001"
538 })
539 .to_string(),
540 )
541 .create_async()
542 .await;
543
544 let seed = [2u8; 32];
545 let config = Config::new(
546 Some(mock_server.url()),
547 480,
548 address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"),
549 "https://world-id-indexer.stage-crypto.worldcoin.org".to_string(),
550 "https://world-id-gateway.stage-crypto.worldcoin.org".to_string(),
551 vec![],
552 2,
553 )
554 .unwrap();
555 let config = serde_json::to_string(&config).unwrap();
556
557 let root = temp_root_path();
558 let provider = InMemoryStorageProvider::new(&root);
559 let store = CredentialStore::from_provider(&provider).expect("store");
560 store.init(42, 100).expect("init storage");
561 cache_embedded_groth16_material(store.storage_paths().expect("paths"))
562 .expect("cache material");
563
564 let paths = store.storage_paths().expect("paths");
565 Authenticator::init(&seed, &config, paths, Arc::new(store))
566 .await
567 .unwrap();
568 drop(mock_server);
569 cleanup_test_storage(&root);
570 }
571}