use crate::{
defaults::DefaultConfig, error::WalletKitError,
primitives::ParseFromForeignBinding, Environment, FieldElement, Region,
};
use alloy_core::primitives::Address;
use ruint::aliases::U256;
use ruint_uniffi::Uint256;
use std::sync::Arc;
use world_id_core::{
api_types::{GatewayErrorCode, GatewayRequestState},
primitives::{AuthenticatorPublicKeySet, Config},
Authenticator as CoreAuthenticator, AuthenticatorConfig,
Credential as CoreCredential, CredentialInput,
InitializingAuthenticator as CoreInitializingAuthenticator,
OnchainKeyRepresentable, Signer,
};
use crate::requests::{ProofRequest, ProofResponse};
use crate::storage::CredentialStore;
#[cfg(not(target_arch = "wasm32"))]
use crate::storage::StoragePaths;
use crate::OwnershipProof;
mod with_storage;
#[derive(Clone, uniffi::Object)]
pub struct Groth16Materials {
query: Arc<world_id_core::proof::CircomGroth16Material>,
nullifier: Arc<world_id_core::proof::CircomGroth16Material>,
}
impl std::fmt::Debug for Groth16Materials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Groth16Materials").finish_non_exhaustive()
}
}
#[cfg(feature = "embed-zkeys")]
#[uniffi::export]
impl Groth16Materials {
#[uniffi::constructor]
pub fn from_embedded() -> Result<Self, WalletKitError> {
let query =
world_id_core::proof::load_embedded_query_material().map_err(|error| {
WalletKitError::Groth16MaterialEmbeddedLoad {
error: error.to_string(),
}
})?;
let nullifier = world_id_core::proof::load_embedded_nullifier_material()
.map_err(|error| WalletKitError::Groth16MaterialEmbeddedLoad {
error: error.to_string(),
})?;
Ok(Self {
query: Arc::new(query),
nullifier: Arc::new(nullifier),
})
}
}
#[cfg(not(target_arch = "wasm32"))]
#[uniffi::export]
impl Groth16Materials {
#[uniffi::constructor]
#[expect(
clippy::needless_pass_by_value,
reason = "UniFFI constructors require owned Arc arguments"
)]
pub fn from_cache(paths: Arc<StoragePaths>) -> Result<Self, 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 = 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(),
})?;
let nullifier = 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(),
})?;
Ok(Self {
query: Arc::new(query),
nullifier: Arc::new(nullifier),
})
}
}
#[derive(Debug, uniffi::Object)]
pub struct Authenticator {
inner: CoreAuthenticator,
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())
}
}
#[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>,
materials: Arc<Groth16Materials>,
store: Arc<CredentialStore>,
) -> Result<Self, WalletKitError> {
let config = Config::from_environment(environment, rpc_url, region)?;
let authenticator = CoreAuthenticator::init(seed, config.into())
.await?
.with_proof_materials(
Arc::clone(&materials.query),
Arc::clone(&materials.nullifier),
);
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,
materials: Arc<Groth16Materials>,
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?
.with_proof_materials(
Arc::clone(&materials.query),
Arc::clone(&materials.nullifier),
);
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 {
#[cfg(target_arch = "wasm32")]
{
return Err(WalletKitError::InvalidInput {
attribute: "now".to_string(),
reason: "`now` must be provided on wasm32 targets".to_string(),
});
}
#[cfg(not(target_arch = "wasm32"))]
{
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())
}
pub async fn prove_credential_sub(
&self,
nonce: &FieldElement,
blinding_factor: &FieldElement,
sub: &FieldElement,
) -> Result<OwnershipProof, WalletKitError> {
#[cfg(target_arch = "wasm32")]
{
let _ = (nonce, blinding_factor, sub);
return Err(WalletKitError::Generic {
error: "credential ownership proofs are not supported on wasm32"
.to_string(),
});
}
#[cfg(not(target_arch = "wasm32"))]
{
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| WalletKitError::Generic {
error: format!("Critical. Unable to determine SystemTime: {e}"),
})?
.as_secs();
let inclusion_proof = self.fetch_inclusion_proof_with_cache(now).await?;
let proof = self
.inner
.prove_credential_sub(
nonce.0,
blinding_factor.0,
sub.0,
Some(inclusion_proof),
)
.await?;
Ok(OwnershipProof(proof))
}
}
}
#[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"));
assert_eq!(material.authenticator_address.len(), 42);
assert!(material.authenticator_pubkey.starts_with("0x"));
assert!(material.authenticator_pubkey.len() <= 66);
assert!(material.offchain_signer_commitment.starts_with("0x"));
assert!(material.offchain_signer_commitment.len() <= 66);
assert!(material.authenticator_address.len() > 2);
assert!(material.authenticator_pubkey.len() > 2);
assert!(material.offchain_signer_commitment.len() > 2);
}
#[test]
fn test_recovery_data_rejects_invalid_seed() {
assert!(RecoveryData::from_seed(&[0u8; 16]).is_err());
assert!(RecoveryData::from_seed(&[]).is_err());
}
#[cfg(feature = "embed-zkeys")]
#[tokio::test]
async fn test_init_with_config_and_materials() {
use crate::storage::tests_utils::{
cleanup_test_storage, temp_root_path, InMemoryStorageProvider,
};
use alloy::primitives::address;
use world_id_core::primitives::Config;
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 config = Config::new(
Some(mock_server.url()),
480,
address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"),
"https://indexer.us.id-infra.worldcoin.dev".to_string(),
"https://gateway.id-infra.worldcoin.dev".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");
let materials =
Arc::new(Groth16Materials::from_embedded().expect("load materials"));
let _authenticator =
Authenticator::init(&[2u8; 32], &config, materials, Arc::new(store))
.await
.unwrap();
drop(mock_server);
cleanup_test_storage(&root);
}
}