use crate::{
defaults::DefaultConfig, error::WalletKitError,
primitives::ParseFromForeignBinding, Environment, FieldElement, Region,
};
use alloy_primitives::Address;
use ruint_uniffi::Uint256;
use std::sync::Arc;
use world_id_core::{
api_types::{GatewayErrorCode, GatewayRequestState},
primitives::Config,
Authenticator as CoreAuthenticator, Credential as CoreCredential,
InitializingAuthenticator as CoreInitializingAuthenticator,
};
#[cfg(feature = "storage")]
use world_id_core::{
requests::{ProofResponse as CoreProofResponse, ResponseItem},
FieldElement as CoreFieldElement,
};
#[cfg(feature = "storage")]
use crate::storage::{CredentialStore, StoragePaths};
#[cfg(feature = "storage")]
use crate::requests::{ProofRequest, ProofResponse};
#[cfg(feature = "storage")]
use rand::rngs::OsRng;
#[cfg(feature = "storage")]
mod with_storage;
type Groth16Materials = (
Arc<world_id_core::proof::CircomGroth16Material>,
Arc<world_id_core::proof::CircomGroth16Material>,
);
#[cfg(not(feature = "storage"))]
fn load_embedded_materials() -> Result<Groth16Materials, WalletKitError> {
let query_material =
world_id_core::proof::load_embedded_query_material().map_err(|error| {
WalletKitError::Groth16MaterialEmbeddedLoad {
error: error.to_string(),
}
})?;
let nullifier_material = world_id_core::proof::load_embedded_nullifier_material()
.map_err(|error| {
WalletKitError::Groth16MaterialEmbeddedLoad {
error: error.to_string(),
}
})?;
Ok((Arc::new(query_material), Arc::new(nullifier_material)))
}
#[cfg(feature = "storage")]
fn load_cached_materials(
paths: &StoragePaths,
) -> Result<Groth16Materials, WalletKitError> {
let query_zkey = paths.query_zkey_path();
let nullifier_zkey = paths.nullifier_zkey_path();
let query_graph = paths.query_graph_path();
let nullifier_graph = paths.nullifier_graph_path();
let query_material = load_query_material_from_cache(&query_zkey, &query_graph)?;
let nullifier_material =
load_nullifier_material_from_cache(&nullifier_zkey, &nullifier_graph)?;
Ok((Arc::new(query_material), Arc::new(nullifier_material)))
}
#[cfg(feature = "storage")]
fn load_query_material_from_cache(
query_zkey: &std::path::Path,
query_graph: &std::path::Path,
) -> Result<world_id_core::proof::CircomGroth16Material, WalletKitError> {
world_id_core::proof::load_query_material_from_paths(query_zkey, query_graph)
.map_err(|error| WalletKitError::Groth16MaterialCacheInvalid {
path: format!(
"{} and {}",
query_zkey.to_string_lossy(),
query_graph.to_string_lossy()
),
error: error.to_string(),
})
}
#[cfg(feature = "storage")]
#[expect(
clippy::unnecessary_wraps,
reason = "Temporary wrapper until world-id-core returns Result for nullifier path loader"
)]
fn load_nullifier_material_from_cache(
nullifier_zkey: &std::path::Path,
nullifier_graph: &std::path::Path,
) -> Result<world_id_core::proof::CircomGroth16Material, WalletKitError> {
Ok(world_id_core::proof::load_nullifier_material_from_paths(
nullifier_zkey,
nullifier_graph,
))
}
#[derive(Debug, uniffi::Object)]
pub struct Authenticator {
inner: CoreAuthenticator,
#[cfg(feature = "storage")]
store: Arc<CredentialStore>,
}
#[uniffi::export(async_runtime = "tokio")]
impl Authenticator {
#[must_use]
pub fn packed_account_data(&self) -> Uint256 {
self.inner.packed_account_data.into()
}
#[must_use]
pub fn leaf_index(&self) -> u64 {
self.inner.leaf_index()
}
#[must_use]
pub fn onchain_address(&self) -> String {
self.inner.onchain_address().to_string()
}
pub async fn get_packed_account_data_remote(
&self,
) -> Result<Uint256, WalletKitError> {
let client = reqwest::Client::new(); let packed_account_data = CoreAuthenticator::get_packed_account_data(
self.inner.onchain_address(),
self.inner.registry().as_deref(),
&self.inner.config,
&client,
)
.await?;
Ok(packed_account_data.into())
}
pub async fn generate_credential_blinding_factor_remote(
&self,
issuer_schema_id: u64,
) -> Result<FieldElement, WalletKitError> {
Ok(self
.inner
.generate_credential_blinding_factor(issuer_schema_id)
.await
.map(Into::into)?)
}
#[must_use]
pub fn compute_credential_sub(
&self,
blinding_factor: &FieldElement,
) -> FieldElement {
CoreCredential::compute_sub(self.inner.leaf_index(), blinding_factor.0).into()
}
}
#[cfg(not(feature = "storage"))]
#[uniffi::export(async_runtime = "tokio")]
impl Authenticator {
#[uniffi::constructor]
pub async fn init_with_defaults(
seed: &[u8],
rpc_url: Option<String>,
environment: &Environment,
region: Option<Region>,
) -> Result<Self, WalletKitError> {
let config = Config::from_environment(environment, rpc_url, region)?;
let authenticator = CoreAuthenticator::init(seed, config).await?;
let (query_material, nullifier_material) = load_embedded_materials()?;
let authenticator =
authenticator.with_proof_materials(query_material, nullifier_material);
Ok(Self {
inner: authenticator,
})
}
#[uniffi::constructor]
pub async fn init(seed: &[u8], config: &str) -> Result<Self, WalletKitError> {
let config =
Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
attribute: "config".to_string(),
reason: "Invalid config".to_string(),
})?;
let authenticator = CoreAuthenticator::init(seed, config).await?;
let (query_material, nullifier_material) = load_embedded_materials()?;
let authenticator =
authenticator.with_proof_materials(query_material, nullifier_material);
Ok(Self {
inner: authenticator,
})
}
}
#[cfg(feature = "storage")]
#[uniffi::export(async_runtime = "tokio")]
impl Authenticator {
#[uniffi::constructor]
pub async fn init_with_defaults(
seed: &[u8],
rpc_url: Option<String>,
environment: &Environment,
region: Option<Region>,
paths: &StoragePaths,
store: Arc<CredentialStore>,
) -> Result<Self, WalletKitError> {
let config = Config::from_environment(environment, rpc_url, region)?;
let authenticator = CoreAuthenticator::init(seed, config).await?;
let (query_material, nullifier_material) = load_cached_materials(paths)?;
let authenticator =
authenticator.with_proof_materials(query_material, nullifier_material);
Ok(Self {
inner: authenticator,
store,
})
}
#[uniffi::constructor]
pub async fn init(
seed: &[u8],
config: &str,
paths: &StoragePaths,
store: Arc<CredentialStore>,
) -> Result<Self, WalletKitError> {
let config =
Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
attribute: "config".to_string(),
reason: "Invalid config".to_string(),
})?;
let authenticator = CoreAuthenticator::init(seed, config).await?;
let (query_material, nullifier_material) = load_cached_materials(paths)?;
let authenticator =
authenticator.with_proof_materials(query_material, nullifier_material);
Ok(Self {
inner: authenticator,
store,
})
}
pub async fn generate_proof(
&self,
proof_request: &ProofRequest,
now: Option<u64>,
) -> Result<ProofResponse, WalletKitError> {
let now = if let Some(n) = now {
n
} else {
let start = std::time::SystemTime::now();
start
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| WalletKitError::Generic {
error: format!("Critical. Unable to determine SystemTime: {e}"),
})?
.as_secs()
};
let credential_list = self.store.list_credentials(None, now)?;
let credential_list = credential_list
.into_iter()
.filter(|cred| !cred.is_expired)
.map(|cred| cred.issuer_schema_id)
.collect::<std::collections::HashSet<_>>();
let credentials_to_prove = proof_request
.0
.credentials_to_prove(&credential_list)
.ok_or(WalletKitError::UnfulfillableRequest)?;
let (inclusion_proof, key_set) =
self.fetch_inclusion_proof_with_cache(now).await?;
let nullifier = self
.inner
.generate_nullifier(&proof_request.0, inclusion_proof, key_set)
.await?;
if self
.store
.is_nullifier_replay(nullifier.verifiable_oprf_output.output.into(), now)?
{
return Err(WalletKitError::NullifierReplay);
}
let mut responses: Vec<ResponseItem> = vec![];
for request_item in credentials_to_prove {
let (credential, blinding_factor) = self
.store
.get_credential(request_item.issuer_schema_id, now)?
.ok_or(WalletKitError::CredentialNotIssued)?;
let session_id_r_seed = CoreFieldElement::random(&mut OsRng);
let response_item = self.inner.generate_single_proof(
nullifier.clone(),
request_item,
&credential,
blinding_factor.0,
session_id_r_seed,
proof_request.0.session_id,
proof_request.0.created_at,
)?;
responses.push(response_item);
}
let response = CoreProofResponse {
id: proof_request.0.id.clone(),
version: world_id_core::requests::RequestVersion::V1,
responses,
error: None,
session_id: None, };
proof_request
.0
.validate_response(&response)
.map_err(|err| WalletKitError::ResponseValidation(err.to_string()))?;
self.store
.replay_guard_set(nullifier.verifiable_oprf_output.output.into(), now)?;
Ok(response.into())
}
}
#[derive(Debug, Clone, uniffi::Enum)]
pub enum RegistrationStatus {
Queued,
Batching,
Submitted,
Finalized,
Failed {
error: String,
error_code: Option<String>,
},
}
impl From<GatewayRequestState> for RegistrationStatus {
fn from(state: GatewayRequestState) -> Self {
match state {
GatewayRequestState::Queued => Self::Queued,
GatewayRequestState::Batching => Self::Batching,
GatewayRequestState::Submitted { .. } => Self::Submitted,
GatewayRequestState::Finalized { .. } => Self::Finalized,
GatewayRequestState::Failed { error, error_code } => Self::Failed {
error,
error_code: error_code.map(|c: GatewayErrorCode| c.to_string()),
},
}
}
}
#[derive(uniffi::Object)]
pub struct InitializingAuthenticator(CoreInitializingAuthenticator);
#[uniffi::export(async_runtime = "tokio")]
impl InitializingAuthenticator {
#[uniffi::constructor]
pub async fn register_with_defaults(
seed: &[u8],
rpc_url: Option<String>,
environment: &Environment,
region: Option<Region>,
recovery_address: Option<String>,
) -> Result<Self, WalletKitError> {
let recovery_address =
Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
let config = Config::from_environment(environment, rpc_url, region)?;
let initializing_authenticator =
CoreAuthenticator::register(seed, config, recovery_address).await?;
Ok(Self(initializing_authenticator))
}
#[uniffi::constructor]
pub async fn register(
seed: &[u8],
config: &str,
recovery_address: Option<String>,
) -> Result<Self, WalletKitError> {
let recovery_address =
Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
let config =
Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
attribute: "config".to_string(),
reason: "Invalid config".to_string(),
})?;
let initializing_authenticator =
CoreAuthenticator::register(seed, config, recovery_address).await?;
Ok(Self(initializing_authenticator))
}
pub async fn poll_status(&self) -> Result<RegistrationStatus, WalletKitError> {
let status = self.0.poll_status().await?;
Ok(status.into())
}
}
#[cfg(all(test, feature = "storage"))]
mod tests {
use super::*;
use crate::storage::cache_embedded_groth16_material;
use crate::storage::tests_utils::{
cleanup_test_storage, temp_root_path, InMemoryStorageProvider,
};
use alloy::primitives::address;
#[tokio::test]
async fn test_init_with_config_and_storage() {
let _ = rustls::crypto::ring::default_provider().install_default();
let mut mock_server = mockito::Server::new_async().await;
mock_server
.mock("POST", "/")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"result": "0x0000000000000000000000000000000000000000000000000000000000000001"
})
.to_string(),
)
.create_async()
.await;
let seed = [2u8; 32];
let config = Config::new(
Some(mock_server.url()),
480,
address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"),
"https://world-id-indexer.stage-crypto.worldcoin.org".to_string(),
"https://world-id-gateway.stage-crypto.worldcoin.org".to_string(),
vec![],
2,
)
.unwrap();
let config = serde_json::to_string(&config).unwrap();
let root = temp_root_path();
let provider = InMemoryStorageProvider::new(&root);
let store = CredentialStore::from_provider(&provider).expect("store");
store.init(42, 100).expect("init storage");
cache_embedded_groth16_material(&store.storage_paths().expect("paths"))
.expect("cache material");
let paths = store.storage_paths().expect("paths");
Authenticator::init(&seed, &config, &paths, Arc::new(store))
.await
.unwrap();
drop(mock_server);
cleanup_test_storage(&root);
}
}