use crate::{
authenticator::artifacts::WalletKitZkArtifactSource, defaults,
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, GatewayRequestId, GatewayRequestState},
primitives::{AuthenticatorPublicKeySet, Config, MAX_AUTHENTICATOR_KEYS},
Authenticator as CoreAuthenticator, AuthenticatorError,
Credential as CoreCredential, CredentialInput, EdDSAPublicKey,
InitializingAuthenticator as CoreInitializingAuthenticator,
OnchainKeyRepresentable, Signer,
};
use crate::requests::{ProofRequest, ProofResponse};
use crate::storage::CredentialStore;
use crate::OwnershipProof;
pub mod artifacts;
mod with_storage;
#[derive(Debug, uniffi::Object)]
pub struct Authenticator {
inner: CoreAuthenticator,
store: Arc<CredentialStore>,
}
impl Authenticator {
pub async fn init_with_config(
seed: &[u8],
config: Config,
artifacts: Arc<dyn WalletKitZkArtifactSource>,
store: Arc<CredentialStore>,
) -> Result<Self, WalletKitError> {
let authenticator = CoreAuthenticator::init(seed, config, artifacts).await?;
Ok(Self {
inner: authenticator,
store,
})
}
}
fn parse_authenticator_pubkey(
attribute: &str,
encoded_pubkey: impl AsRef<str>,
) -> Result<EdDSAPublicKey, WalletKitError> {
let encoded_pubkey = encoded_pubkey.as_ref();
let invalid_input = |reason: String| WalletKitError::InvalidInput {
attribute: attribute.to_string(),
reason,
};
let hex = encoded_pubkey.strip_prefix("0x").ok_or_else(|| {
invalid_input("Public key must start with a 0x prefix".to_string())
})?;
if hex.len() != 64 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(invalid_input(
"Public key must be exactly 32 bytes (64 hex characters) after the 0x prefix"
.to_string(),
));
}
let encoded = U256::from_str_radix(hex, 16)
.map_err(|error| invalid_input(error.to_string()))?;
let pubkey = EdDSAPublicKey::from_compressed_bytes(encoded.to_le_bytes())
.map_err(|error| invalid_input(error.to_string()))?;
let canonical = pubkey
.to_ethereum_representation()
.map_err(|error| invalid_input(error.to_string()))?;
if canonical != encoded {
return Err(invalid_input(
"Public key is not the canonical compressed point encoding".to_string(),
));
}
if canonical == U256::from(1u64) {
return Err(invalid_input(
"Public key must not be the BabyJubJub identity point".to_string(),
));
}
Ok(pubkey)
}
#[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()
}
#[allow(
clippy::needless_pass_by_value,
reason = "seed is passed by value so uniffi 0.32 maps it to a `RustBuffer` (Kotlin `ByteArray` / Swift `Data`) rather than the non-`Send` `ForeignBytes` view produced for `&[u8]`"
)]
pub fn danger_sign_challenge(
&self,
challenge: Vec<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 update_recovery_agent(
&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.update_recovery_agent(new_recovery_agent).await?;
Ok(request_id.to_string())
}
pub async fn revert_recovery_agent_update(&self) -> Result<String, WalletKitError> {
let request_id = self.inner.revert_recovery_agent_update().await?;
Ok(request_id.to_string())
}
#[tracing::instrument(
target = "walletkit_latency",
name = "gateway_insert_authenticator",
skip_all
)]
pub async fn insert_authenticator(
&self,
new_authenticator_pubkey: String,
new_authenticator_address: String,
) -> Result<String, WalletKitError> {
let new_authenticator_pubkey = parse_authenticator_pubkey(
"new_authenticator_pubkey",
new_authenticator_pubkey,
)?;
let new_authenticator_address = Address::parse_from_ffi(
&new_authenticator_address,
"new_authenticator_address",
)?;
let request_id = self
.inner
.insert_authenticator(new_authenticator_pubkey, new_authenticator_address)
.await?;
Ok(request_id.to_string())
}
#[tracing::instrument(
target = "walletkit_latency",
name = "indexer_authenticator_pubkeys",
skip_all
)]
pub async fn has_authenticator_pubkey(
&self,
authenticator_pubkey: String,
) -> Result<bool, WalletKitError> {
let authenticator_pubkey =
parse_authenticator_pubkey("authenticator_pubkey", authenticator_pubkey)?;
let pubkeys = self.inner.fetch_authenticator_pubkeys().await?;
Ok(pubkeys
.iter()
.flatten()
.any(|existing_pubkey| existing_pubkey == &authenticator_pubkey))
}
#[tracing::instrument(
target = "walletkit_latency",
name = "indexer_authenticator_pubkeys",
skip_all
)]
pub async fn get_authenticator_pubkeys(
&self,
) -> Result<Vec<Option<String>>, WalletKitError> {
let key_set = self.inner.fetch_authenticator_pubkeys().await?;
key_set
.iter()
.map(|slot| {
slot.as_ref()
.map(|pubkey| {
let encoded = pubkey.to_ethereum_representation()?;
Ok(format!("{encoded:#066x}"))
})
.transpose()
})
.collect()
}
#[tracing::instrument(
target = "walletkit_latency",
name = "gateway_remove_authenticator",
skip_all
)]
pub async fn remove_authenticator(
&self,
authenticator_address: String,
pubkey_id: u32,
expected_authenticator_pubkey: String,
) -> Result<String, WalletKitError> {
let expected_pubkey = parse_authenticator_pubkey(
"expected_authenticator_pubkey",
expected_authenticator_pubkey,
)?;
let authenticator_address =
Address::parse_from_ffi(&authenticator_address, "authenticator_address")?;
if pubkey_id as usize >= MAX_AUTHENTICATOR_KEYS {
return Err(WalletKitError::InvalidInput {
attribute: "pubkey_id".to_string(),
reason: format!(
"pubkey_id {pubkey_id} is out of range; the key set has at \
most {MAX_AUTHENTICATOR_KEYS} slots"
),
});
}
let empty_slot = || WalletKitError::InvalidInput {
attribute: "pubkey_id".to_string(),
reason: format!("no authenticator at key set slot {pubkey_id}"),
};
let key_set = self.inner.fetch_authenticator_pubkeys().await?;
let actual_pubkey = key_set.get(pubkey_id as usize).ok_or_else(empty_slot)?;
if actual_pubkey != &expected_pubkey {
return Err(WalletKitError::InvalidInput {
attribute: "expected_authenticator_pubkey".to_string(),
reason: format!(
"key set slot {pubkey_id} holds a different authenticator public key"
),
});
}
let request_id = self
.inner
.remove_authenticator(authenticator_address, pubkey_id)
.await
.map_err(|error| match error {
AuthenticatorError::PublicKeyNotFound => empty_slot(),
other => other.into(),
})?;
Ok(request_id.to_string())
}
#[tracing::instrument(
target = "walletkit_latency",
name = "gateway_poll",
skip_all
)]
pub async fn poll_status(
&self,
request_id: String,
) -> Result<GatewayRequestStatus, WalletKitError> {
let request_id = GatewayRequestId::new(
request_id.strip_prefix("gw_").unwrap_or(&request_id),
);
let status = self.inner.poll_status(&request_id).await?;
Ok(status.into())
}
}
#[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: Vec<u8>,
rpc_url: Option<String>,
environment: &Environment,
region: Option<Region>,
artifacts: Arc<dyn WalletKitZkArtifactSource>,
store: Arc<CredentialStore>,
) -> Result<Self, WalletKitError> {
let config = defaults::default_config(environment, rpc_url, region)?;
Self::init_with_config(&seed, config, artifacts, store).await
}
#[uniffi::constructor]
#[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
pub async fn init_with_ohttp_defaults(
seed: Vec<u8>,
rpc_url: Option<String>,
environment: &Environment,
region: Option<Region>,
artifacts: Arc<dyn WalletKitZkArtifactSource>,
store: Arc<CredentialStore>,
) -> Result<Self, WalletKitError> {
let config = defaults::default_config_with_ohttp(environment, rpc_url, region)?;
Self::init_with_config(&seed, config, artifacts, store).await
}
#[uniffi::constructor]
#[tracing::instrument(target = "walletkit_latency", name = "rpc_init", skip_all)]
pub async fn init(
seed: Vec<u8>,
config: &str,
artifacts: Arc<dyn WalletKitZkArtifactSource>,
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(),
})?;
Self::init_with_config(&seed, config, artifacts, store).await
}
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,
now,
Some(account_inclusion_proof.clone()),
))
.await?;
if self
.store
.is_nullifier_replay(nullifier.verifiable_oprf_output.output.into(), now)?
{
return Err(WalletKitError::NullifierReplay);
}
let rp_id = proof_request.0.rp_id.into_inner();
let session_id_r_seed =
proof_request
.0
.session_id
.existing()
.and_then(|session_id| match self.store.get_session_seed(
rp_id,
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) = result.proof_response.session_id {
if let Err(err) = self.store.store_session_seed(
rp_id,
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,
context: &FieldElement,
blinding_factor: &FieldElement,
sub: &FieldElement,
) -> Result<OwnershipProof, WalletKitError> {
#[cfg(target_arch = "wasm32")]
{
let _ = (nonce, context, 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,
context.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>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
pub enum GatewayRequestStatus {
Queued,
Batching,
Submitted {
tx_hash: String,
},
Finalized {
tx_hash: String,
},
Failed {
error: String,
error_code: Option<String>,
},
}
impl From<GatewayRequestState> for GatewayRequestStatus {
fn from(state: GatewayRequestState) -> Self {
match state {
GatewayRequestState::Queued => Self::Queued,
GatewayRequestState::Batching => Self::Batching,
GatewayRequestState::Submitted { tx_hash } => Self::Submitted { tx_hash },
GatewayRequestState::Finalized { tx_hash } => Self::Finalized { tx_hash },
GatewayRequestState::Failed { error, error_code } => Self::Failed {
error,
error_code: error_code.map(|code| code.to_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: Vec<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 = defaults::default_config(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_with_ohttp_defaults(
seed: Vec<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 = defaults::default_config_with_ohttp(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: Vec<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))
}
#[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 validate_authenticator_pubkey(
authenticator_pubkey: &str,
) -> Result<String, WalletKitError> {
let pubkey =
parse_authenticator_pubkey("authenticator_pubkey", authenticator_pubkey)?;
let encoded = pubkey.to_ethereum_representation()?;
Ok(format!("{encoded:#066x}"))
}
#[uniffi::export]
#[allow(
clippy::needless_pass_by_value,
reason = "seed is passed by value so uniffi 0.32 maps it to a `RustBuffer` (Kotlin `ByteArray` / Swift `Data`) rather than the non-`Send` `ForeignBytes` view produced for `&[u8]`"
)]
pub fn recovery_data_from_seed(seed: Vec<u8>) -> Result<RecoveryData, WalletKitError> {
RecoveryData::from_seed(&seed)
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_SEED: [u8; 32] = [1u8; 32];
async fn test_authenticator(
server: &mut mockito::Server,
) -> (Authenticator, std::path::PathBuf) {
use crate::storage::tests_utils::{temp_root_path, InMemoryStorageProvider};
use alloy::primitives::address;
use world_id_core::primitives::ServiceEndpoint;
use world_id_proof::artifacts::dummy::DummyZkArtifactSource;
let _ = rustls::crypto::ring::default_provider().install_default();
let packed_account_mock = server
.mock("POST", "/packed-account")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::json!({ "packed_account_data": "0x2a" }).to_string())
.create_async()
.await;
let config = Config::new(
None,
480,
address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"),
ServiceEndpoint::direct(server.url()),
ServiceEndpoint::direct(server.url()),
vec![],
2,
)
.expect("valid config");
let root = temp_root_path();
let provider = InMemoryStorageProvider::new(&root);
let store =
CredentialStore::from_provider(&provider).expect("credential store");
let authenticator = Authenticator::init_with_config(
&TEST_SEED,
config,
Arc::new(DummyZkArtifactSource),
Arc::new(store),
)
.await
.expect("authenticator should initialize");
packed_account_mock.assert_async().await;
(authenticator, root)
}
fn encoded_pubkey(seed: &[u8; 32]) -> String {
let pubkey = Signer::from_seed_bytes(seed)
.expect("valid seed")
.offchain_signer_pubkey()
.to_ethereum_representation()
.expect("public key should encode");
format!("{pubkey:#066x}")
}
async fn mock_authenticator_pubkeys(
server: &mut mockito::Server,
pubkeys: &[Option<&str>],
expected_hits: usize,
) -> mockito::Mock {
server
.mock("POST", "/authenticator-pubkeys")
.match_body(mockito::Matcher::JsonString(
serde_json::json!({ "leaf_index": "0x2a" }).to_string(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({
"authenticator_pubkeys": pubkeys,
"offchain_signer_commitment": "0x0"
})
.to_string(),
)
.expect(expected_hits)
.create_async()
.await
}
#[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());
}
#[test]
fn test_authenticator_pubkey_validation() {
let canonical = encoded_pubkey(&[2u8; 32]);
assert_eq!(
validate_authenticator_pubkey(&canonical).expect("valid key"),
canonical
);
let uppercase = format!("0x{}", canonical[2..].to_uppercase());
assert_eq!(
validate_authenticator_pubkey(&uppercase)
.expect("uppercase hex should canonicalize"),
canonical
);
for invalid_pubkey in [
"not-a-public-key".to_string(),
format!("0x{}", "ff".repeat(32)),
] {
assert!(matches!(
validate_authenticator_pubkey(&invalid_pubkey),
Err(WalletKitError::InvalidInput { attribute, .. })
if attribute == "authenticator_pubkey"
));
}
let identity = format!("0x{}01", "0".repeat(62));
assert!(matches!(
validate_authenticator_pubkey(&identity),
Err(WalletKitError::InvalidInput { attribute, reason })
if attribute == "authenticator_pubkey" && reason.contains("identity")
));
let sign_bit_alias = format!("0x80{}01", "0".repeat(60));
assert!(matches!(
validate_authenticator_pubkey(&sign_bit_alias),
Err(WalletKitError::InvalidInput { attribute, reason })
if attribute == "authenticator_pubkey" && reason.contains("canonical")
));
}
#[tokio::test]
async fn test_poll_status_normalizes_request_id() {
use crate::storage::tests_utils::cleanup_test_storage;
let mut server = mockito::Server::new_async().await;
let (authenticator, root) = test_authenticator(&mut server).await;
let status_mock = server
.mock("GET", "/status/gw_poll_test")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({
"request_id": "gw_poll_test",
"kind": "insert_authenticator",
"status": {
"state": "finalized",
"tx_hash": "0x1234"
}
})
.to_string(),
)
.expect(2)
.create_async()
.await;
for request_id in ["poll_test", "gw_poll_test"] {
assert_eq!(
authenticator
.poll_status(request_id.to_string())
.await
.expect("status poll should succeed"),
GatewayRequestStatus::Finalized {
tx_hash: "0x1234".to_string()
}
);
}
status_mock.assert_async().await;
drop(server);
cleanup_test_storage(&root);
}
#[tokio::test]
async fn test_remove_authenticator_refuses_unexpected_slot_contents() {
use crate::storage::tests_utils::cleanup_test_storage;
let mut server = mockito::Server::new_async().await;
let (authenticator, root) = test_authenticator(&mut server).await;
let existing_pubkey = encoded_pubkey(&TEST_SEED);
let slot_pubkey = encoded_pubkey(&[2u8; 32]);
let pubkeys_mock = mock_authenticator_pubkeys(
&mut server,
&[
Some(existing_pubkey.as_str()),
None,
Some(slot_pubkey.as_str()),
],
2,
)
.await;
let nonce_mock = server
.mock("POST", "/signature-nonce")
.expect(0)
.create_async()
.await;
let remove_mock = server
.mock("POST", "/remove-authenticator")
.expect(0)
.create_async()
.await;
let mismatched = authenticator
.remove_authenticator(
Address::ZERO.to_string(),
2,
encoded_pubkey(&[3u8; 32]),
)
.await;
assert!(matches!(
mismatched,
Err(WalletKitError::InvalidInput { attribute, .. })
if attribute == "expected_authenticator_pubkey"
));
let empty_slot = authenticator
.remove_authenticator(
Address::ZERO.to_string(),
1,
encoded_pubkey(&[3u8; 32]),
)
.await;
assert!(matches!(
empty_slot,
Err(WalletKitError::InvalidInput { attribute, reason })
if attribute == "pubkey_id"
&& reason.contains("no authenticator at key set slot 1")
));
let out_of_range = authenticator
.remove_authenticator(
Address::ZERO.to_string(),
7,
encoded_pubkey(&[3u8; 32]),
)
.await;
assert!(matches!(
out_of_range,
Err(WalletKitError::InvalidInput { attribute, reason })
if attribute == "pubkey_id" && reason.contains("out of range")
));
pubkeys_mock.assert_async().await;
nonce_mock.assert_async().await;
remove_mock.assert_async().await;
drop(server);
cleanup_test_storage(&root);
}
#[tokio::test]
async fn test_key_set_reads_return_slots_and_membership() {
use crate::storage::tests_utils::cleanup_test_storage;
let mut server = mockito::Server::new_async().await;
let (authenticator, root) = test_authenticator(&mut server).await;
let existing_pubkey = encoded_pubkey(&TEST_SEED);
let other_pubkey = encoded_pubkey(&[2u8; 32]);
let pubkeys_mock = mock_authenticator_pubkeys(
&mut server,
&[
Some(existing_pubkey.as_str()),
None,
Some(other_pubkey.as_str()),
],
3,
)
.await;
assert!(authenticator
.has_authenticator_pubkey(existing_pubkey.clone())
.await
.expect("membership read should succeed"));
assert!(!authenticator
.has_authenticator_pubkey(encoded_pubkey(&[3u8; 32]))
.await
.expect("absent key check should succeed"));
assert_eq!(
authenticator
.get_authenticator_pubkeys()
.await
.expect("key set read should succeed"),
vec![Some(existing_pubkey), None, Some(other_pubkey)]
);
pubkeys_mock.assert_async().await;
drop(server);
cleanup_test_storage(&root);
}
#[tokio::test]
async fn test_remove_authenticator_reports_slot_emptied_during_signing() {
use crate::storage::tests_utils::cleanup_test_storage;
use std::sync::atomic::{AtomicUsize, Ordering};
let mut server = mockito::Server::new_async().await;
let (authenticator, root) = test_authenticator(&mut server).await;
let existing_pubkey = encoded_pubkey(&TEST_SEED);
let removed_pubkey = encoded_pubkey(&[2u8; 32]);
let full_body = serde_json::json!({
"authenticator_pubkeys": [existing_pubkey.clone(), removed_pubkey.clone()],
"offchain_signer_commitment": "0x0"
})
.to_string();
let emptied_body = serde_json::json!({
"authenticator_pubkeys": [existing_pubkey],
"offchain_signer_commitment": "0x0"
})
.to_string();
let fetches = Arc::new(AtomicUsize::new(0));
let fetches_in_mock = Arc::clone(&fetches);
let pubkeys_mock = server
.mock("POST", "/authenticator-pubkeys")
.with_status(200)
.with_header("content-type", "application/json")
.with_body_from_request(move |_request| {
if fetches_in_mock.fetch_add(1, Ordering::SeqCst) == 0 {
full_body.clone().into_bytes()
} else {
emptied_body.clone().into_bytes()
}
})
.expect(2)
.create_async()
.await;
let nonce_mock = server
.mock("POST", "/signature-nonce")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::json!({ "signature_nonce": "0x1" }).to_string())
.create_async()
.await;
let remove_mock = server
.mock("POST", "/remove-authenticator")
.expect(0)
.create_async()
.await;
let raced = authenticator
.remove_authenticator(Address::ZERO.to_string(), 1, removed_pubkey)
.await;
assert!(matches!(
raced,
Err(WalletKitError::InvalidInput { attribute, .. })
if attribute == "pubkey_id"
));
pubkeys_mock.assert_async().await;
nonce_mock.assert_async().await;
remove_mock.assert_async().await;
drop(server);
cleanup_test_storage(&root);
}
#[cfg(feature = "embed-zkeys")]
#[tokio::test]
async fn test_init_with_config_and_materials() {
use crate::{
authenticator::artifacts::caching::CachingZkArtifacts,
storage::tests_utils::{
cleanup_test_storage, temp_root_path, InMemoryStorageProvider,
},
};
use alloy::primitives::address;
use world_id_core::primitives::{Config, ServiceEndpoint};
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"),
ServiceEndpoint::direct(
"https://indexer.us.id-infra.worldcoin.dev".to_string(),
),
ServiceEndpoint::direct(
"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 artifacts =
Arc::new(CachingZkArtifacts::new(Arc::new(store.paths().unwrap())));
let _authenticator = Authenticator::init(
[2u8; 32].to_vec(),
&config,
artifacts,
Arc::new(store),
)
.await
.unwrap();
drop(mock_server);
cleanup_test_storage(&root);
}
}