use crate::{
defaults::DefaultConfig, error::WalletKitError,
primitives::ParseFromForeignBinding, Environment, FieldElement, Region,
};
use alloy_primitives::Address;
use ruint::aliases::U256;
use ruint_uniffi::Uint256;
use std::sync::Arc;
use world_id_core::{
api_types::{GatewayErrorCode, GatewayRequestState},
primitives::AuthenticatorPublicKeySet,
Authenticator as CoreAuthenticator, AuthenticatorConfig,
Credential as CoreCredential,
InitializingAuthenticator as CoreInitializingAuthenticator,
OnchainKeyRepresentable, Signer,
};
#[cfg(feature = "storage")]
use world_id_core::CredentialInput;
#[cfg(feature = "storage")]
use crate::storage::{CredentialStore, StoragePaths};
#[cfg(feature = "storage")]
use crate::requests::{ProofRequest, ProofResponse};
#[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")]
fn load_nullifier_material_from_cache(
nullifier_zkey: &std::path::Path,
nullifier_graph: &std::path::Path,
) -> Result<world_id_core::proof::CircomGroth16Material, WalletKitError> {
world_id_core::proof::load_nullifier_material_from_paths(
nullifier_zkey,
nullifier_graph,
)
.map_err(|error| WalletKitError::Groth16MaterialCacheInvalid {
path: format!(
"{} and {}",
nullifier_zkey.to_string_lossy(),
nullifier_graph.to_string_lossy()
),
error: error.to_string(),
})
}
#[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()
}
#[tracing::instrument(
target = "walletkit_latency",
name = "rpc_account_data",
skip_all
)]
pub async fn get_packed_account_data_remote(
&self,
) -> Result<Uint256, WalletKitError> {
let packed_account_data = self.inner.fetch_packed_account_data().await?;
Ok(packed_account_data.into())
}
#[tracing::instrument(
target = "walletkit_latency",
name = "oprf_blinding_factor",
skip_all
)]
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()
}
pub fn danger_sign_challenge(
&self,
challenge: &[u8],
) -> Result<Vec<u8>, WalletKitError> {
let signature = self.inner.danger_sign_challenge(challenge)?;
Ok(signature.as_bytes().to_vec())
}
pub async fn danger_sign_initiate_recovery_agent_update(
&self,
new_recovery_agent: String,
) -> Result<RecoveryUpdateSignature, WalletKitError> {
let new_recovery_agent =
Address::parse_from_ffi(&new_recovery_agent, "new_recovery_agent")?;
let (sig, nonce) = self
.inner
.danger_sign_initiate_recovery_agent_update(new_recovery_agent)
.await?;
Ok(RecoveryUpdateSignature {
signature: sig.as_bytes().to_vec(),
nonce: nonce.into(),
})
}
pub async fn initiate_recovery_agent_update(
&self,
new_recovery_agent: String,
) -> Result<String, WalletKitError> {
let new_recovery_agent =
Address::parse_from_ffi(&new_recovery_agent, "new_recovery_agent")?;
let request_id = self
.inner
.initiate_recovery_agent_update(new_recovery_agent)
.await?;
Ok(request_id.to_string())
}
pub async fn execute_recovery_agent_update(
&self,
) -> Result<String, WalletKitError> {
let request_id = self.inner.execute_recovery_agent_update().await?;
Ok(request_id.to_string())
}
pub async fn cancel_recovery_agent_update(&self) -> Result<String, WalletKitError> {
let request_id = self.inner.cancel_recovery_agent_update().await?;
Ok(request_id.to_string())
}
}
#[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 =
AuthenticatorConfig::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 = AuthenticatorConfig::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]
#[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
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 =
AuthenticatorConfig::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]
#[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
pub async fn init(
seed: &[u8],
config: &str,
paths: &StoragePaths,
store: Arc<CredentialStore>,
) -> Result<Self, WalletKitError> {
let config = AuthenticatorConfig::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 credentials: Vec<_> = self
.store
.list_credentials(None, now)?
.iter()
.filter(|c| !c.is_expired)
.filter_map(|cred| {
if let Ok(Some((credential, blinding_factor))) =
self.store.get_credential(cred.issuer_schema_id, now)
{
Some(CredentialInput {
credential: credential.into(),
blinding_factor: blinding_factor.into(),
})
} else {
tracing::warn!(
issuer_schema_id = %cred.issuer_schema_id,
credential_id = %cred.credential_id,
"credential listed but not loadable, skipping"
);
None
}
})
.collect();
let account_inclusion_proof =
self.fetch_inclusion_proof_with_cache(now).await?;
let nullifier = Box::pin(self.inner.generate_nullifier(
&proof_request.0,
Some(account_inclusion_proof.clone()),
))
.await?;
if self
.store
.is_nullifier_replay(nullifier.verifiable_oprf_output.output.into(), now)?
{
return Err(WalletKitError::NullifierReplay);
}
let session_id_r_seed =
proof_request
.0
.session_id
.and_then(|session_id| {
match self.store.get_session_seed(session_id.oprf_seed, now) {
Ok(seed) => seed,
Err(err) => {
tracing::warn!(error = %err, "failed to load cached session seed, continuing without");
None
}
}
});
let result = Box::pin(self.inner.generate_proof(
&proof_request.0,
nullifier.clone(),
&credentials,
Some(account_inclusion_proof),
session_id_r_seed,
))
.await?;
if let Some(seed) = result.session_id_r_seed {
if let Some(session_id) = proof_request.0.session_id {
if let Err(err) =
self.store
.store_session_seed(session_id.oprf_seed, seed, now)
{
tracing::error!("error caching session_id_r_seed: {}", err);
}
}
}
self.store
.replay_guard_set(nullifier.verifiable_oprf_output.output.into(), now)?;
Ok(result.proof_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]
#[tracing::instrument(
target = "walletkit_latency",
name = "gateway_register",
skip_all
)]
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 =
AuthenticatorConfig::from_environment(environment, rpc_url, region)?;
let initializing_authenticator =
CoreAuthenticator::register(seed, config, recovery_address).await?;
Ok(Self(initializing_authenticator))
}
#[uniffi::constructor]
#[tracing::instrument(
target = "walletkit_latency",
name = "gateway_register",
skip_all
)]
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 = AuthenticatorConfig::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))
}
#[tracing::instrument(
target = "walletkit_latency",
name = "gateway_poll",
skip_all
)]
pub async fn poll_status(&self) -> Result<RegistrationStatus, WalletKitError> {
let status = self.0.poll_status().await?;
Ok(status.into())
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct RecoveryUpdateSignature {
pub signature: Vec<u8>,
pub nonce: Uint256,
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct RecoveryData {
pub authenticator_address: String,
pub authenticator_pubkey: String,
pub offchain_signer_commitment: String,
}
impl RecoveryData {
pub fn from_seed(seed: &[u8]) -> Result<Self, WalletKitError> {
let signer = Signer::from_seed_bytes(seed)?;
let authenticator_address = signer.onchain_signer_address().to_checksum(None);
let authenticator_pubkey: U256 = signer
.offchain_signer_pubkey()
.to_ethereum_representation()?;
let mut key_set = AuthenticatorPublicKeySet::default();
key_set.try_push(signer.offchain_signer_pubkey())?;
let offchain_signer_commitment: U256 = key_set.leaf_hash().into();
Ok(Self {
authenticator_address,
authenticator_pubkey: format!("{authenticator_pubkey:#066x}"),
offchain_signer_commitment: format!("{offchain_signer_commitment:#066x}"),
})
}
}
#[uniffi::export]
pub fn recovery_data_from_seed(seed: &[u8]) -> Result<RecoveryData, WalletKitError> {
RecoveryData::from_seed(seed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recovery_data_from_seed() {
let seed = [1u8; 32];
let material = RecoveryData::from_seed(&seed).expect("should derive material");
assert!(
material.authenticator_address.starts_with("0x"),
"address should start with 0x"
);
assert_eq!(
material.authenticator_address.len(),
42,
"address should be 42 chars (0x + 40 hex digits)"
);
assert!(
material.authenticator_pubkey.starts_with("0x"),
"pubkey should start with 0x"
);
assert!(
material.authenticator_pubkey.len() <= 66,
"pubkey should be at most 66 chars (0x + 64 hex digits)"
);
assert!(
material.offchain_signer_commitment.starts_with("0x"),
"commitment should start with 0x"
);
assert!(
material.offchain_signer_commitment.len() <= 66,
"commitment should be at most 66 chars (0x + 64 hex digits)"
);
assert!(
material.authenticator_address.len() > 2,
"address should be non-empty"
);
assert!(
material.authenticator_pubkey.len() > 2,
"pubkey should be non-empty"
);
assert!(
material.offchain_signer_commitment.len() > 2,
"commitment should be non-empty"
);
}
#[test]
fn test_recovery_data_rejects_invalid_seed() {
let result = RecoveryData::from_seed(&[0u8; 16]);
assert!(result.is_err(), "should reject 16-byte seed");
let result = RecoveryData::from_seed(&[]);
assert!(result.is_err(), "should reject empty seed");
}
}
#[cfg(all(test, feature = "storage"))]
mod storage_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;
use world_id_core::primitives::Config;
async fn init_test_authenticator(
seed: &[u8],
) -> (Authenticator, std::path::PathBuf) {
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 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");
let authenticator = Authenticator::init(seed, &config, &paths, Arc::new(store))
.await
.unwrap();
drop(mock_server);
(authenticator, root)
}
#[tokio::test]
async fn test_init_with_config_and_storage() {
let _ = rustls::crypto::ring::default_provider().install_default();
let (_, root) = init_test_authenticator(&[2u8; 32]).await;
cleanup_test_storage(&root);
}
}