use crate::{
error::{AuthenticatorError, PollResult},
init::InitializingAuthenticator,
};
use std::sync::Arc;
use crate::{
api_types::{
AccountInclusionProof, GatewayRequestState, IndexerAuthenticatorPubkeysResponse,
IndexerErrorCode, IndexerPackedAccountRequest, IndexerPackedAccountResponse,
IndexerQueryRequest, IndexerSignatureNonceResponse, ServiceApiError,
},
service_client::{ServiceClient, ServiceKind},
};
use serde::{Deserialize, Serialize};
use world_id_primitives::{Credential, FieldElement, ProofResponse, Signer};
pub use crate::ohttp::OhttpClientConfig;
use crate::registry::WorldIdRegistry::WorldIdRegistryInstance;
use alloy::{
primitives::Address,
providers::DynProvider,
signers::{Signature, SignerSync},
};
use ark_serialize::CanonicalSerialize;
use eddsa_babyjubjub::EdDSAPublicKey;
use groth16_material::circom::CircomGroth16Material;
use ruint::{aliases::U256, uint};
use taceo_oprf::client::Connector;
pub use world_id_primitives::{Config, TREE_DEPTH, authenticator::ProtocolSigner};
use world_id_primitives::{
PrimitiveError,
authenticator::{
AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError,
decode_sparse_authenticator_pubkeys,
},
};
#[expect(unused_imports, reason = "used for docs")]
use world_id_primitives::{Nullifier, SessionId};
static MASK_RECOVERY_COUNTER: U256 =
uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
static MASK_PUBKEY_ID: U256 =
uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
static MASK_LEAF_INDEX: U256 =
uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthenticatorConfig {
#[serde(flatten)]
pub config: Config,
#[serde(default)]
pub ohttp_indexer: Option<OhttpClientConfig>,
#[serde(default)]
pub ohttp_gateway: Option<OhttpClientConfig>,
}
impl AuthenticatorConfig {
pub fn from_json(json_str: &str) -> Result<Self, AuthenticatorError> {
serde_json::from_str(json_str).map_err(|e| {
AuthenticatorError::from(PrimitiveError::Serialization(format!(
"failed to parse authenticator config: {e}"
)))
})
}
}
impl From<Config> for AuthenticatorConfig {
fn from(config: Config) -> Self {
Self {
config,
ohttp_indexer: None,
ohttp_gateway: None,
}
}
}
pub struct CredentialInput {
pub credential: Credential,
pub blinding_factor: FieldElement,
}
#[derive(Debug)]
pub struct ProofResult {
pub session_id_r_seed: Option<FieldElement>,
pub proof_response: ProofResponse,
}
pub struct Authenticator {
pub config: Config,
pub packed_account_data: U256,
pub(crate) signer: Signer,
pub(crate) registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
pub(crate) indexer_client: ServiceClient,
pub(crate) gateway_client: ServiceClient,
pub(crate) ws_connector: Connector,
pub(crate) query_material: Option<Arc<CircomGroth16Material>>,
pub(crate) nullifier_material: Option<Arc<CircomGroth16Material>>,
}
impl std::fmt::Debug for Authenticator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Authenticator")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl Authenticator {
pub async fn init(
seed: &[u8],
config: AuthenticatorConfig,
) -> Result<Self, AuthenticatorError> {
let AuthenticatorConfig {
config,
ohttp_indexer,
ohttp_gateway,
} = config;
let signer = Signer::from_seed_bytes(seed)?;
let registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>> =
config.rpc_url().map(|rpc_url| {
let provider = alloy::providers::ProviderBuilder::new()
.with_chain_id(config.chain_id())
.connect_http(rpc_url.clone());
Arc::new(crate::registry::WorldIdRegistry::new(
*config.registry_address(),
alloy::providers::Provider::erased(provider),
))
});
let http_client = reqwest::Client::new();
let indexer_client = ServiceClient::new(
http_client.clone(),
ServiceKind::Indexer,
config.indexer_url(),
ohttp_indexer,
)?;
let gateway_client = ServiceClient::new(
http_client,
ServiceKind::Gateway,
config.gateway_url(),
ohttp_gateway,
)?;
let packed_account_data = Self::get_packed_account_data(
signer.onchain_signer_address(),
registry.as_deref(),
&config,
&indexer_client,
)
.await?;
#[cfg(not(target_arch = "wasm32"))]
let ws_connector = {
let mut root_store = rustls::RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let rustls_config = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
Connector::Rustls(Arc::new(rustls_config))
};
#[cfg(target_arch = "wasm32")]
let ws_connector = Connector;
Ok(Self {
packed_account_data,
signer,
config,
registry,
indexer_client,
gateway_client,
ws_connector,
query_material: None,
nullifier_material: None,
})
}
#[must_use]
pub fn with_proof_materials(
self,
query_material: Arc<CircomGroth16Material>,
nullifier_material: Arc<CircomGroth16Material>,
) -> Self {
Self {
query_material: Some(query_material),
nullifier_material: Some(nullifier_material),
..self
}
}
pub async fn register(
seed: &[u8],
config: AuthenticatorConfig,
recovery_address: Option<Address>,
) -> Result<InitializingAuthenticator, AuthenticatorError> {
let AuthenticatorConfig {
config,
ohttp_gateway,
..
} = config;
let gateway_client = ServiceClient::new(
reqwest::Client::new(),
ServiceKind::Gateway,
config.gateway_url(),
ohttp_gateway,
)?;
InitializingAuthenticator::new(seed, config, recovery_address, gateway_client).await
}
pub async fn init_or_register(
seed: &[u8],
config: AuthenticatorConfig,
recovery_address: Option<Address>,
) -> Result<Self, AuthenticatorError> {
match Self::init(seed, config.clone()).await {
Ok(authenticator) => Ok(authenticator),
Err(AuthenticatorError::AccountDoesNotExist) => {
let gateway_client = ServiceClient::new(
reqwest::Client::new(),
ServiceKind::Gateway,
config.config.gateway_url(),
config.ohttp_gateway.clone(),
)?;
let initializing_authenticator = InitializingAuthenticator::new(
seed,
config.config.clone(),
recovery_address,
gateway_client,
)
.await?;
let backoff = backon::ExponentialBuilder::default()
.with_min_delay(std::time::Duration::from_millis(800))
.with_factor(1.5)
.without_max_times()
.with_total_delay(Some(std::time::Duration::from_secs(120)));
let poller = || async {
let poll_status = initializing_authenticator.poll_status().await;
let result = match poll_status {
Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
Ok(GatewayRequestState::Failed { error_code, error }) => Err(
PollResult::TerminalError(AuthenticatorError::RegistrationError {
error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
error_message: error,
}),
),
Err(AuthenticatorError::GatewayError { status, body }) => {
if status.is_client_error() {
Err(PollResult::TerminalError(
AuthenticatorError::GatewayError { status, body },
))
} else {
Err(PollResult::Retryable)
}
}
_ => Err(PollResult::Retryable),
};
match result {
Ok(()) => match Self::init(seed, config.clone()).await {
Ok(auth) => Ok(auth),
Err(AuthenticatorError::AccountDoesNotExist) => {
Err(PollResult::Retryable)
}
Err(e) => Err(PollResult::TerminalError(e)),
},
Err(e) => Err(e),
}
};
let result = backon::Retryable::retry(poller, backoff)
.when(|e| matches!(e, PollResult::Retryable))
.await;
match result {
Ok(authenticator) => Ok(authenticator),
Err(PollResult::TerminalError(e)) => Err(e),
Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
}
}
Err(e) => Err(e),
}
}
pub async fn refresh_packed_account_data(&self) -> Result<U256, AuthenticatorError> {
Self::get_packed_account_data(
self.onchain_address(),
self.registry().as_deref(),
&self.config,
&self.indexer_client,
)
.await
}
async fn get_packed_account_data(
onchain_signer_address: Address,
registry: Option<&WorldIdRegistryInstance<DynProvider>>,
config: &Config,
indexer_client: &ServiceClient,
) -> Result<U256, AuthenticatorError> {
let raw_index = if let Some(registry) = registry {
registry
.getPackedAccountData(onchain_signer_address)
.call()
.await?
} else {
let req = IndexerPackedAccountRequest {
authenticator_address: onchain_signer_address,
};
match indexer_client
.post_json::<_, IndexerPackedAccountResponse>(
config.indexer_url(),
"/packed-account",
&req,
)
.await
{
Ok(response) => response.packed_account_data,
Err(AuthenticatorError::IndexerError { status, body }) => {
if let Ok(error_resp) =
serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
{
return match error_resp.code {
IndexerErrorCode::AccountDoesNotExist => {
Err(AuthenticatorError::AccountDoesNotExist)
}
_ => Err(AuthenticatorError::IndexerError {
status,
body: error_resp.message,
}),
};
}
return Err(AuthenticatorError::IndexerError { status, body });
}
Err(other) => return Err(other),
}
};
if raw_index == U256::ZERO {
return Err(AuthenticatorError::AccountDoesNotExist);
}
Ok(raw_index)
}
#[must_use]
pub const fn onchain_address(&self) -> Address {
self.signer.onchain_signer_address()
}
#[must_use]
pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
self.signer.offchain_signer_pubkey()
}
pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
let pk = self.signer.offchain_signer_pubkey().pk;
let mut compressed_bytes = Vec::new();
pk.serialize_compressed(&mut compressed_bytes)
.map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
Ok(U256::from_le_slice(&compressed_bytes))
}
#[must_use]
pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
self.registry.clone()
}
#[must_use]
pub fn leaf_index(&self) -> u64 {
(self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
}
#[must_use]
pub fn recovery_counter(&self) -> U256 {
let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
recovery_counter >> 224
}
#[must_use]
pub fn pubkey_id(&self) -> U256 {
let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
pubkey_id >> 192
}
pub async fn fetch_inclusion_proof(
&self,
) -> Result<AccountInclusionProof<TREE_DEPTH>, AuthenticatorError> {
let req = IndexerQueryRequest {
leaf_index: self.leaf_index(),
};
let response: AccountInclusionProof<TREE_DEPTH> = self
.indexer_client
.post_json(self.config.indexer_url(), "/inclusion-proof", &req)
.await?;
Ok(response)
}
pub async fn fetch_authenticator_pubkeys(
&self,
) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
let req = IndexerQueryRequest {
leaf_index: self.leaf_index(),
};
let response: IndexerAuthenticatorPubkeysResponse = self
.indexer_client
.post_json(self.config.indexer_url(), "/authenticator-pubkeys", &req)
.await?;
Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
}
pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
let registry = self.registry();
if let Some(registry) = registry {
let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
Ok(nonce)
} else {
let req = IndexerQueryRequest {
leaf_index: self.leaf_index(),
};
let response: IndexerSignatureNonceResponse = self
.indexer_client
.post_json(self.config.indexer_url(), "/signature-nonce", &req)
.await?;
Ok(response.signature_nonce)
}
}
pub fn danger_sign_challenge(&self, challenge: &[u8]) -> Result<Signature, AuthenticatorError> {
self.signer
.onchain_signer()
.sign_message_sync(challenge)
.map_err(|e| AuthenticatorError::Generic(format!("signature error: {e}")))
}
pub(crate) fn decode_indexer_pubkeys(
pubkeys: Vec<Option<U256>>,
) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
SparseAuthenticatorPubkeysError::SlotOutOfBounds {
slot_index,
max_supported_slot,
} => AuthenticatorError::InvalidIndexerPubkeySlot {
slot_index,
max_supported_slot,
},
SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
PrimitiveError::Deserialization(format!(
"invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
))
.into()
}
})
}
pub(crate) fn insert_or_reuse_authenticator_key(
key_set: &mut AuthenticatorPublicKeySet,
new_authenticator_pubkey: EdDSAPublicKey,
) -> Result<usize, AuthenticatorError> {
if let Some(index) = key_set.iter().position(Option::is_none) {
key_set.try_set_at_index(index, new_authenticator_pubkey)?;
Ok(index)
} else {
key_set.try_push(new_authenticator_pubkey)?;
Ok(key_set.len() - 1)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{error::AuthenticatorError, traits::OnchainKeyRepresentable};
use alloy::primitives::{U256, address};
use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
Signer::from_seed_bytes(&[seed_byte; 32])
.unwrap()
.offchain_signer_pubkey()
}
fn encoded_test_pubkey(seed_byte: u8) -> U256 {
test_pubkey(seed_byte).to_ethereum_representation().unwrap()
}
#[test]
fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
let mut key_set =
AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
.unwrap();
key_set[1] = None;
let new_key = test_pubkey(3);
let index =
Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
assert_eq!(index, 1);
assert_eq!(key_set.len(), 3);
assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
}
#[test]
fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
let new_key = test_pubkey(2);
let index =
Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
assert_eq!(index, 1);
assert_eq!(key_set.len(), 2);
assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
}
#[test]
fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
assert_eq!(key_set.len(), 2);
assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
}
#[test]
fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
assert!(matches!(
error,
AuthenticatorError::InvalidIndexerPubkeySlot {
slot_index,
max_supported_slot
} if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
));
}
#[tokio::test]
async fn test_get_packed_account_data_from_indexer() {
let mut server = mockito::Server::new_async().await;
let indexer_url = server.url();
let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
let expected_packed_index = U256::from(42);
let mock = server
.mock("POST", "/packed-account")
.match_header("content-type", "application/json")
.match_body(mockito::Matcher::JsonString(
serde_json::json!({ "authenticator_address": test_address }).to_string(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({ "packed_account_data": format!("{:#x}", expected_packed_index) }).to_string(),
)
.create_async()
.await;
let config = Config::new(
None,
1,
address!("0x0000000000000000000000000000000000000001"),
indexer_url,
"http://gateway.example.com".to_string(),
Vec::new(),
2,
)
.unwrap();
let indexer_client = ServiceClient::new(
reqwest::Client::new(),
ServiceKind::Indexer,
config.indexer_url(),
None,
)
.unwrap();
let result = Authenticator::get_packed_account_data(
test_address,
None, &config,
&indexer_client,
)
.await
.unwrap();
assert_eq!(result, expected_packed_index);
mock.assert_async().await;
drop(server);
}
#[tokio::test]
async fn test_get_packed_account_data_from_indexer_error() {
let mut server = mockito::Server::new_async().await;
let indexer_url = server.url();
let test_address = address!("0x0000000000000000000000000000000000000099");
let mock = server
.mock("POST", "/packed-account")
.with_status(400)
.with_header("content-type", "application/json")
.with_body(serde_json::json!({ "code": "account_does_not_exist", "message": "There is no account for this authenticator address" }).to_string())
.create_async()
.await;
let config = Config::new(
None,
1,
address!("0x0000000000000000000000000000000000000001"),
indexer_url,
"http://gateway.example.com".to_string(),
Vec::new(),
2,
)
.unwrap();
let indexer_client = ServiceClient::new(
reqwest::Client::new(),
ServiceKind::Indexer,
config.indexer_url(),
None,
)
.unwrap();
let result =
Authenticator::get_packed_account_data(test_address, None, &config, &indexer_client)
.await;
assert!(matches!(
result,
Err(AuthenticatorError::AccountDoesNotExist)
));
mock.assert_async().await;
drop(server);
}
#[tokio::test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_signing_nonce_from_indexer() {
let mut server = mockito::Server::new_async().await;
let indexer_url = server.url();
let leaf_index = U256::from(1);
let expected_nonce = U256::from(5);
let mock = server
.mock("POST", "/signature-nonce")
.match_header("content-type", "application/json")
.match_body(mockito::Matcher::JsonString(
serde_json::json!({ "leaf_index": format!("{:#x}", leaf_index) }).to_string(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({ "signature_nonce": format!("{:#x}", expected_nonce) })
.to_string(),
)
.create_async()
.await;
let config = Config::new(
None,
1,
address!("0x0000000000000000000000000000000000000001"),
indexer_url,
"http://gateway.example.com".to_string(),
Vec::new(),
2,
)
.unwrap();
let http_client = reqwest::Client::new();
let authenticator = Authenticator {
config: config.clone(),
packed_account_data: leaf_index,
signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
registry: None,
indexer_client: ServiceClient::new(
http_client.clone(),
ServiceKind::Indexer,
config.indexer_url(),
None,
)
.unwrap(),
gateway_client: ServiceClient::new(
http_client,
ServiceKind::Gateway,
config.gateway_url(),
None,
)
.unwrap(),
ws_connector: Connector::Plain,
query_material: None,
nullifier_material: None,
};
let nonce = authenticator.signing_nonce().await.unwrap();
assert_eq!(nonce, expected_nonce);
mock.assert_async().await;
drop(server);
}
#[test]
fn test_danger_sign_challenge_returns_valid_signature() {
let config = Config::new(
None,
1,
address!("0x0000000000000000000000000000000000000001"),
"http://indexer.example.com".to_string(),
"http://gateway.example.com".to_string(),
Vec::new(),
2,
)
.unwrap();
let http_client = reqwest::Client::new();
let authenticator = Authenticator {
indexer_client: ServiceClient::new(
http_client.clone(),
ServiceKind::Indexer,
config.indexer_url(),
None,
)
.unwrap(),
gateway_client: ServiceClient::new(
http_client,
ServiceKind::Gateway,
config.gateway_url(),
None,
)
.unwrap(),
config,
packed_account_data: U256::from(1),
signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
registry: None,
ws_connector: Connector::Plain,
query_material: None,
nullifier_material: None,
};
let challenge = b"test challenge";
let signature = authenticator.danger_sign_challenge(challenge).unwrap();
let recovered = signature
.recover_address_from_msg(challenge)
.expect("should recover address");
assert_eq!(recovered, authenticator.onchain_address());
}
#[test]
fn test_danger_sign_challenge_different_challenges_different_signatures() {
let config = Config::new(
None,
1,
address!("0x0000000000000000000000000000000000000001"),
"http://indexer.example.com".to_string(),
"http://gateway.example.com".to_string(),
Vec::new(),
2,
)
.unwrap();
let http_client = reqwest::Client::new();
let authenticator = Authenticator {
indexer_client: ServiceClient::new(
http_client.clone(),
ServiceKind::Indexer,
config.indexer_url(),
None,
)
.unwrap(),
gateway_client: ServiceClient::new(
http_client,
ServiceKind::Gateway,
config.gateway_url(),
None,
)
.unwrap(),
config,
packed_account_data: U256::from(1),
signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
registry: None,
ws_connector: Connector::Plain,
query_material: None,
nullifier_material: None,
};
let sig_a = authenticator.danger_sign_challenge(b"challenge A").unwrap();
let sig_b = authenticator.danger_sign_challenge(b"challenge B").unwrap();
assert_ne!(sig_a, sig_b);
}
#[test]
fn test_danger_sign_challenge_deterministic() {
let config = Config::new(
None,
1,
address!("0x0000000000000000000000000000000000000001"),
"http://indexer.example.com".to_string(),
"http://gateway.example.com".to_string(),
Vec::new(),
2,
)
.unwrap();
let http_client = reqwest::Client::new();
let authenticator = Authenticator {
indexer_client: ServiceClient::new(
http_client.clone(),
ServiceKind::Indexer,
config.indexer_url(),
None,
)
.unwrap(),
gateway_client: ServiceClient::new(
http_client,
ServiceKind::Gateway,
config.gateway_url(),
None,
)
.unwrap(),
config,
packed_account_data: U256::from(1),
signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
registry: None,
ws_connector: Connector::Plain,
query_material: None,
nullifier_material: None,
};
let challenge = b"deterministic test";
let sig1 = authenticator.danger_sign_challenge(challenge).unwrap();
let sig2 = authenticator.danger_sign_challenge(challenge).unwrap();
assert_eq!(sig1, sig2);
}
#[tokio::test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_signing_nonce_from_indexer_error() {
let mut server = mockito::Server::new_async().await;
let indexer_url = server.url();
let mock = server
.mock("POST", "/signature-nonce")
.with_status(400)
.with_header("content-type", "application/json")
.with_body(serde_json::json!({ "code": "invalid_leaf_index", "message": "Account index cannot be zero" }).to_string())
.create_async()
.await;
let config = Config::new(
None,
1,
address!("0x0000000000000000000000000000000000000001"),
indexer_url,
"http://gateway.example.com".to_string(),
Vec::new(),
2,
)
.unwrap();
let http_client = reqwest::Client::new();
let authenticator = Authenticator {
config: config.clone(),
packed_account_data: U256::ZERO,
signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
registry: None,
indexer_client: ServiceClient::new(
http_client.clone(),
ServiceKind::Indexer,
config.indexer_url(),
None,
)
.unwrap(),
gateway_client: ServiceClient::new(
http_client,
ServiceKind::Gateway,
config.gateway_url(),
None,
)
.unwrap(),
ws_connector: Connector::Plain,
query_material: None,
nullifier_material: None,
};
let result = authenticator.signing_nonce().await;
assert!(matches!(
result,
Err(AuthenticatorError::IndexerError { .. })
));
mock.assert_async().await;
drop(server);
}
#[test]
fn test_authenticator_config_from_json_plain_config() {
let json = serde_json::json!({
"chain_id": 480,
"registry_address": "0x0000000000000000000000000000000000000001",
"indexer_url": "http://indexer.example.com",
"gateway_url": "http://gateway.example.com",
"nullifier_oracle_urls": [],
"nullifier_oracle_threshold": 2
});
let config = AuthenticatorConfig::from_json(&json.to_string()).unwrap();
assert!(config.ohttp_indexer.is_none());
assert!(config.ohttp_gateway.is_none());
assert_eq!(config.config.gateway_url(), "http://gateway.example.com");
}
#[test]
fn test_authenticator_config_from_json_with_ohttp() {
let json = serde_json::json!({
"chain_id": 480,
"registry_address": "0x0000000000000000000000000000000000000001",
"indexer_url": "http://indexer.example.com",
"gateway_url": "http://gateway.example.com",
"nullifier_oracle_urls": [],
"nullifier_oracle_threshold": 2,
"ohttp_indexer": {
"relay_url": "https://relay.example.com/gateway",
"key_config_base64": "dGVzdC1rZXk="
},
"ohttp_gateway": {
"relay_url": "https://relay.example.com/gateway",
"key_config_base64": "dGVzdC1rZXk="
}
});
let config = AuthenticatorConfig::from_json(&json.to_string()).unwrap();
let ohttp_indexer = config.ohttp_indexer.unwrap();
assert_eq!(ohttp_indexer.relay_url, "https://relay.example.com/gateway");
assert_eq!(ohttp_indexer.key_config_base64, "dGVzdC1rZXk=");
let ohttp_gateway = config.ohttp_gateway.unwrap();
assert_eq!(ohttp_gateway.relay_url, "https://relay.example.com/gateway");
assert_eq!(ohttp_gateway.key_config_base64, "dGVzdC1rZXk=");
}
}