From 4cb4432cc4494ddc1145675308b48d20ca703ebb Mon Sep 17 00:00:00 2001 From: Otto Date: Tue, 17 Mar 2026 14:36:29 +0000 Subject: [PATCH 1/7] feat: expose authenticator management (insert, update, remove) in WalletKit Add methods to the WalletKit Authenticator for managing authenticators on a World ID account, matching the world-id-protocol's authenticator management capabilities: - insert_authenticator: Add a new authenticator to the account - update_authenticator: Replace an existing authenticator at a slot - remove_authenticator: Remove an authenticator from a slot - poll_operation_status: Track gateway operation finalization Implementation uses pure delegation to CoreAuthenticator's management methods (now &self in world-id-core 0.5.3). WalletKit only handles FFI type conversion (compressed pubkey bytes -> EdDSAPublicKey, hex string -> Address). Depends on: worldcoin/world-id-protocol#564 (0.5.3) Tests: - Unit tests for pubkey parsing helper (valid, invalid, roundtrip) - Integration tests for poll_operation_status (success + error) - All existing tests pass (82 total) --- walletkit-core/src/authenticator/mod.rs | 350 +++++++++++++++++++++++- 1 file changed, 347 insertions(+), 3 deletions(-) diff --git a/walletkit-core/src/authenticator/mod.rs b/walletkit-core/src/authenticator/mod.rs index 90b03f47..d315c5bb 100644 --- a/walletkit-core/src/authenticator/mod.rs +++ b/walletkit-core/src/authenticator/mod.rs @@ -8,10 +8,10 @@ use alloy_primitives::Address; use ruint_uniffi::Uint256; use std::sync::Arc; use world_id_core::{ - api_types::{GatewayErrorCode, GatewayRequestState}, + api_types::{GatewayErrorCode, GatewayRequestState, GatewayStatusResponse}, primitives::Config, Authenticator as CoreAuthenticator, Credential as CoreCredential, - InitializingAuthenticator as CoreInitializingAuthenticator, + EdDSAPublicKey, InitializingAuthenticator as CoreInitializingAuthenticator, }; #[cfg(feature = "storage")] @@ -121,6 +121,29 @@ fn load_nullifier_material_from_cache( )) } +/// Parses a compressed off-chain `EdDSA` public key from a 32-byte little-endian byte slice. +/// +/// # Errors +/// Returns an error if the bytes are not exactly 32 bytes or cannot be decompressed. +fn parse_compressed_pubkey( + bytes: &[u8], +) -> Result { + let compressed: [u8; 32] = + bytes.try_into().map_err(|_| WalletKitError::InvalidInput { + attribute: "new_authenticator_pubkey_bytes".to_string(), + reason: format!( + "Expected 32 bytes for compressed public key, got {}", + bytes.len() + ), + })?; + EdDSAPublicKey::from_compressed_bytes(compressed).map_err(|e| { + WalletKitError::InvalidInput { + attribute: "new_authenticator_pubkey_bytes".to_string(), + reason: format!("Invalid compressed public key: {e}"), + } + }) +} + /// The Authenticator is the main component with which users interact with the World ID Protocol. #[derive(Debug, uniffi::Object)] pub struct Authenticator { @@ -220,6 +243,137 @@ impl Authenticator { let signature = self.inner.danger_sign_challenge(challenge)?; Ok(signature.as_bytes().to_vec()) } + + /// Inserts a new authenticator to the account. + /// + /// The current authenticator signs the request to authorize adding a new authenticator. + /// The new authenticator will be registered in the `WorldIDRegistry` contract and can + /// subsequently sign operations on behalf of the same World ID. + /// + /// # Arguments + /// * `new_authenticator_pubkey_bytes` - The compressed off-chain `EdDSA` public key of the new + /// authenticator (32 bytes, little-endian). + /// * `new_authenticator_address` - The on-chain Ethereum address (hex string) of the new + /// authenticator. + /// + /// # Returns + /// A gateway request ID that can be used with [`poll_operation_status`](Self::poll_operation_status) + /// to track the on-chain finalization of the operation. + /// + /// # Errors + /// - Will error if the compressed public key bytes are invalid or not 32 bytes. + /// - Will error if the address string is not a valid hex address. + /// - Will error if there are network issues communicating with the indexer or gateway. + /// - Will error if the maximum number of authenticators has been reached. + pub async fn insert_authenticator( + &self, + new_authenticator_pubkey_bytes: Vec, + new_authenticator_address: String, + ) -> Result { + let new_address = + Address::parse_from_ffi(&new_authenticator_address, "new_authenticator_address")?; + let new_pubkey = parse_compressed_pubkey(&new_authenticator_pubkey_bytes)?; + Ok(self.inner.insert_authenticator(new_pubkey, new_address).await?) + } + + /// Updates an existing authenticator slot with a new authenticator. + /// + /// The current authenticator signs the request to authorize replacing the authenticator + /// at the specified slot index. + /// + /// # Arguments + /// * `old_authenticator_address` - The on-chain address (hex string) of the authenticator being replaced. + /// * `new_authenticator_address` - The on-chain address (hex string) of the new authenticator. + /// * `new_authenticator_pubkey_bytes` - The compressed off-chain `EdDSA` public key of the new + /// authenticator (32 bytes, little-endian). + /// * `index` - The pubkey slot index of the authenticator being replaced. + /// + /// # Returns + /// A gateway request ID that can be used with [`poll_operation_status`](Self::poll_operation_status). + /// + /// # Errors + /// - Will error if the compressed public key bytes are invalid. + /// - Will error if the address strings are not valid hex addresses. + /// - Will error if the index is out of bounds. + /// - Will error if there are network issues. + pub async fn update_authenticator( + &self, + old_authenticator_address: String, + new_authenticator_address: String, + new_authenticator_pubkey_bytes: Vec, + index: u32, + ) -> Result { + let old_address = + Address::parse_from_ffi(&old_authenticator_address, "old_authenticator_address")?; + let new_address = + Address::parse_from_ffi(&new_authenticator_address, "new_authenticator_address")?; + let new_pubkey = parse_compressed_pubkey(&new_authenticator_pubkey_bytes)?; + Ok(self.inner.update_authenticator(old_address, new_address, new_pubkey, index).await?) + } + + /// Removes an authenticator from the account. + /// + /// The current authenticator signs the request to authorize removing the authenticator + /// at the specified slot index. An authenticator can remove itself or any other authenticator + /// on the same account. + /// + /// # Arguments + /// * `authenticator_address` - The on-chain address (hex string) of the authenticator to remove. + /// * `index` - The pubkey slot index of the authenticator being removed. + /// + /// # Returns + /// A gateway request ID that can be used with [`poll_operation_status`](Self::poll_operation_status). + /// + /// # Errors + /// - Will error if the address string is not a valid hex address. + /// - Will error if the index is out of bounds or there is no authenticator at that slot. + /// - Will error if there are network issues. + pub async fn remove_authenticator( + &self, + authenticator_address: String, + index: u32, + ) -> Result { + let auth_address = + Address::parse_from_ffi(&authenticator_address, "authenticator_address")?; + Ok(self.inner.remove_authenticator(auth_address, index).await?) + } + + /// Polls the status of a gateway operation (insert, update, or remove authenticator). + /// + /// Use the request ID returned by [`insert_authenticator`](Self::insert_authenticator), + /// [`update_authenticator`](Self::update_authenticator), or + /// [`remove_authenticator`](Self::remove_authenticator) to track the operation. + /// + /// # Errors + /// Will error if the network request fails or the gateway returns an error. + pub async fn poll_operation_status( + &self, + request_id: String, + ) -> Result { + let url = format!( + "{}/status/{}", + self.inner.config.gateway_url(), + request_id + ); + let client = reqwest::Client::new(); // TODO: reuse client + let resp = client.get(&url).send().await?; + let status = resp.status(); + + if status.is_success() { + let body: GatewayStatusResponse = resp.json().await?; + Ok(body.status.into()) + } else { + let body = resp + .text() + .await + .unwrap_or_else(|e| format!("Unable to read response body: {e}")); + Err(WalletKitError::NetworkError { + url, + error: body, + status: Some(status.as_u16()), + }) + } + } } #[cfg(not(feature = "storage"))] @@ -534,8 +688,61 @@ impl InitializingAuthenticator { } } -#[cfg(all(test, feature = "storage"))] +#[cfg(test)] mod tests { + use super::*; + use world_id_core::OnchainKeyRepresentable; + + fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey { + let signer = world_id_core::primitives::Signer::from_seed_bytes(&[seed_byte; 32]) + .unwrap(); + signer.offchain_signer_pubkey() + } + + fn compressed_pubkey_bytes(seed_byte: u8) -> Vec { + let pk = test_pubkey(seed_byte); + let u256 = pk.to_ethereum_representation().unwrap(); + u256.to_le_bytes_vec() + } + + // ── Compressed pubkey parsing ── + + #[test] + fn test_parse_compressed_pubkey_valid() { + let bytes = compressed_pubkey_bytes(1); + assert_eq!(bytes.len(), 32); + let result = parse_compressed_pubkey(&bytes); + assert!(result.is_ok()); + } + + #[test] + fn test_parse_compressed_pubkey_wrong_length() { + let short = vec![0u8; 16]; + let result = parse_compressed_pubkey(&short); + assert!(matches!( + result, + Err(WalletKitError::InvalidInput { attribute, .. }) + if attribute == "new_authenticator_pubkey_bytes" + )); + } + + #[test] + fn test_parse_compressed_pubkey_roundtrip() { + let original = test_pubkey(42); + let bytes = { + let u256 = original.to_ethereum_representation().unwrap(); + u256.to_le_bytes_vec() + }; + let recovered = parse_compressed_pubkey(&bytes).unwrap(); + assert_eq!(original.pk, recovered.pk); + } + +} + +// ── Storage-dependent tests ── + +#[cfg(all(test, feature = "storage"))] +mod storage_tests { use super::*; use crate::storage::cache_embedded_groth16_material; use crate::storage::tests_utils::{ @@ -593,4 +800,141 @@ mod tests { drop(mock_server); cleanup_test_storage(&root); } + + #[tokio::test] + async fn test_poll_operation_status_finalized() { + let mut server = mockito::Server::new_async().await; + let mock = server + .mock("GET", "/status/req_abc") + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + serde_json::json!({ + "request_id": "req_abc", + "kind": "insert_authenticator", + "status": { "state": "finalized", "tx_hash": "0x1234" } + }) + .to_string(), + ) + .create_async() + .await; + + // Create an Authenticator pointing at this mock server + let mut rpc_server = mockito::Server::new_async().await; + rpc_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 = [3u8; 32]; + let config = Config::new( + Some(rpc_server.url()), + 480, + address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"), + "https://unused-indexer.example.com".to_string(), + server.url(), + 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 auth = Authenticator::init(&seed, &config, paths, Arc::new(store)) + .await + .unwrap(); + + let status = auth + .poll_operation_status("req_abc".to_string()) + .await + .unwrap(); + assert!(matches!(status, RegistrationStatus::Finalized)); + + mock.assert_async().await; + drop(server); + drop(rpc_server); + cleanup_test_storage(&root); + } + + #[tokio::test] + async fn test_poll_operation_status_gateway_error() { + let mut server = mockito::Server::new_async().await; + let mock = server + .mock("GET", "/status/req_bad") + .with_status(500) + .with_body("internal server error") + .create_async() + .await; + + let mut rpc_server = mockito::Server::new_async().await; + rpc_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 = [4u8; 32]; + let config = Config::new( + Some(rpc_server.url()), + 480, + address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"), + "https://unused-indexer.example.com".to_string(), + server.url(), + 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 auth = Authenticator::init(&seed, &config, paths, Arc::new(store)) + .await + .unwrap(); + + let result = auth + .poll_operation_status("req_bad".to_string()) + .await; + assert!(matches!( + result, + Err(WalletKitError::NetworkError { status: Some(500), .. }) + )); + + mock.assert_async().await; + drop(server); + drop(rpc_server); + cleanup_test_storage(&root); + } } From beeaa7f2258ff54eb90c73dc66b1b0bc92e8cdd3 Mon Sep 17 00:00:00 2001 From: Otto Date: Tue, 17 Mar 2026 20:36:58 +0000 Subject: [PATCH 2/7] chore: switch world-id-core to published 0.5.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace git branch reference (fix/authenticator-management-self-ref) with the published crate: world-id-core = { version = "0.5.4", default-features = false } - world-id-authenticator, world-id-primitives, world-id-proof all updated to 0.5.4 in Cargo.lock accordingly - Fix test signal field: Option → Option> per updated API Verified: cargo build + cargo clippy pass clean; 82/82 unit tests pass (test_authenticator_integration requires anvil binary — pre-existing env gap) --- walletkit-core/src/authenticator/mod.rs | 65 ++++++++++++++----------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/walletkit-core/src/authenticator/mod.rs b/walletkit-core/src/authenticator/mod.rs index d315c5bb..6988a42b 100644 --- a/walletkit-core/src/authenticator/mod.rs +++ b/walletkit-core/src/authenticator/mod.rs @@ -10,8 +10,8 @@ use std::sync::Arc; use world_id_core::{ api_types::{GatewayErrorCode, GatewayRequestState, GatewayStatusResponse}, primitives::Config, - Authenticator as CoreAuthenticator, Credential as CoreCredential, - EdDSAPublicKey, InitializingAuthenticator as CoreInitializingAuthenticator, + Authenticator as CoreAuthenticator, Credential as CoreCredential, EdDSAPublicKey, + InitializingAuthenticator as CoreInitializingAuthenticator, }; #[cfg(feature = "storage")] @@ -125,9 +125,7 @@ fn load_nullifier_material_from_cache( /// /// # Errors /// Returns an error if the bytes are not exactly 32 bytes or cannot be decompressed. -fn parse_compressed_pubkey( - bytes: &[u8], -) -> Result { +fn parse_compressed_pubkey(bytes: &[u8]) -> Result { let compressed: [u8; 32] = bytes.try_into().map_err(|_| WalletKitError::InvalidInput { attribute: "new_authenticator_pubkey_bytes".to_string(), @@ -270,10 +268,15 @@ impl Authenticator { new_authenticator_pubkey_bytes: Vec, new_authenticator_address: String, ) -> Result { - let new_address = - Address::parse_from_ffi(&new_authenticator_address, "new_authenticator_address")?; + let new_address = Address::parse_from_ffi( + &new_authenticator_address, + "new_authenticator_address", + )?; let new_pubkey = parse_compressed_pubkey(&new_authenticator_pubkey_bytes)?; - Ok(self.inner.insert_authenticator(new_pubkey, new_address).await?) + Ok(self + .inner + .insert_authenticator(new_pubkey, new_address) + .await?) } /// Updates an existing authenticator slot with a new authenticator. @@ -303,12 +306,19 @@ impl Authenticator { new_authenticator_pubkey_bytes: Vec, index: u32, ) -> Result { - let old_address = - Address::parse_from_ffi(&old_authenticator_address, "old_authenticator_address")?; - let new_address = - Address::parse_from_ffi(&new_authenticator_address, "new_authenticator_address")?; + let old_address = Address::parse_from_ffi( + &old_authenticator_address, + "old_authenticator_address", + )?; + let new_address = Address::parse_from_ffi( + &new_authenticator_address, + "new_authenticator_address", + )?; let new_pubkey = parse_compressed_pubkey(&new_authenticator_pubkey_bytes)?; - Ok(self.inner.update_authenticator(old_address, new_address, new_pubkey, index).await?) + Ok(self + .inner + .update_authenticator(old_address, new_address, new_pubkey, index) + .await?) } /// Removes an authenticator from the account. @@ -350,11 +360,7 @@ impl Authenticator { &self, request_id: String, ) -> Result { - let url = format!( - "{}/status/{}", - self.inner.config.gateway_url(), - request_id - ); + let url = format!("{}/status/{}", self.inner.config.gateway_url(), request_id); let client = reqwest::Client::new(); // TODO: reuse client let resp = client.get(&url).send().await?; let status = resp.status(); @@ -694,8 +700,9 @@ mod tests { use world_id_core::OnchainKeyRepresentable; fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey { - let signer = world_id_core::primitives::Signer::from_seed_bytes(&[seed_byte; 32]) - .unwrap(); + let signer = + world_id_core::primitives::Signer::from_seed_bytes(&[seed_byte; 32]) + .unwrap(); signer.offchain_signer_pubkey() } @@ -736,7 +743,6 @@ mod tests { let recovered = parse_compressed_pubkey(&bytes).unwrap(); assert_eq!(original.pk, recovered.pk); } - } // ── Storage-dependent tests ── @@ -853,11 +859,11 @@ mod storage_tests { 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")) + cache_embedded_groth16_material(&store.storage_paths().expect("paths")) .expect("cache material"); let paths = store.storage_paths().expect("paths"); - let auth = Authenticator::init(&seed, &config, paths, Arc::new(store)) + let auth = Authenticator::init(&seed, &config, &paths, Arc::new(store)) .await .unwrap(); @@ -916,20 +922,21 @@ mod storage_tests { 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")) + cache_embedded_groth16_material(&store.storage_paths().expect("paths")) .expect("cache material"); let paths = store.storage_paths().expect("paths"); - let auth = Authenticator::init(&seed, &config, paths, Arc::new(store)) + let auth = Authenticator::init(&seed, &config, &paths, Arc::new(store)) .await .unwrap(); - let result = auth - .poll_operation_status("req_bad".to_string()) - .await; + let result = auth.poll_operation_status("req_bad".to_string()).await; assert!(matches!( result, - Err(WalletKitError::NetworkError { status: Some(500), .. }) + Err(WalletKitError::NetworkError { + status: Some(500), + .. + }) )); mock.assert_async().await; From e131f6e2a7d2b140309189145aaf6da274b151cc Mon Sep 17 00:00:00 2001 From: Otto Date: Wed, 25 Mar 2026 15:19:04 +0000 Subject: [PATCH 3/7] chore: update world-id-core to 0.7.0 (GatewayRequestId + poll_status) --- Cargo.lock | 58 ++++++++++++++++++------- Cargo.toml | 2 +- walletkit-core/src/authenticator/mod.rs | 12 +++-- walletkit-core/src/credential.rs | 12 ++--- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f11b572a..a84a8589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5220,17 +5220,26 @@ name = "taceo-oprf" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be862ba49094098f945f1375f704a2c47b4e267a6e564462a43ad142b4b1469e" +dependencies = [ + "taceo-oprf-types 0.10.1", +] + +[[package]] +name = "taceo-oprf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f2d8a89181967875c3e130dca3d5e906e720b30568b82c68653c3048b1bb02" dependencies = [ "taceo-oprf-client", "taceo-oprf-core", - "taceo-oprf-types", + "taceo-oprf-types 0.11.0", ] [[package]] name = "taceo-oprf-client" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696c4b03e8b570d1d0486f7bc2273b85951b316d7f720fc2f95e79f2b053f649" +checksum = "b1624edfd12063146c6f3614c659cb178a8ae82bfac72b751a72133136000866" dependencies = [ "ark-ec", "ciborium", @@ -5241,7 +5250,7 @@ dependencies = [ "serde", "taceo-ark-babyjubjub", "taceo-oprf-core", - "taceo-oprf-types", + "taceo-oprf-types 0.11.0", "taceo-poseidon2", "thiserror 2.0.18", "tokio", @@ -5293,6 +5302,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "taceo-oprf-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92ebe8693dfb75f2f1e7861785792747495f8f2be058ff07698f16046cb5fad" +dependencies = [ + "alloy", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "async-trait", + "eyre", + "http", + "serde", + "taceo-ark-babyjubjub", + "taceo-ark-serde-compat", + "taceo-circom-types", + "taceo-groth16-sol", + "taceo-oprf-core", + "uuid", +] + [[package]] name = "taceo-poseidon2" version = "0.2.1" @@ -6079,7 +6109,7 @@ dependencies = [ "sha2", "strum", "subtle", - "taceo-oprf", + "taceo-oprf 0.8.0", "thiserror 2.0.18", "tokio", "tokio-test", @@ -6689,8 +6719,7 @@ dependencies = [ [[package]] name = "world-id-authenticator" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d061f3e1150294b0eaab3c3a8311343c144995097c6f08cd7babbe8610faca" +source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" dependencies = [ "alloy", "anyhow", @@ -6707,7 +6736,7 @@ dependencies = [ "taceo-ark-babyjubjub", "taceo-eddsa-babyjubjub", "taceo-groth16-material", - "taceo-oprf", + "taceo-oprf 0.10.0", "taceo-poseidon2", "thiserror 2.0.18", "tokio", @@ -6719,8 +6748,7 @@ dependencies = [ [[package]] name = "world-id-core" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88fbe3660167654a014924faeeec87d7aebb9ff13a43dbb51958045f0aa3ed6" +source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" dependencies = [ "taceo-eddsa-babyjubjub", "world-id-authenticator", @@ -6731,8 +6759,7 @@ dependencies = [ [[package]] name = "world-id-primitives" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022157c68f1c49bb3d7256b9199174d6de6cc9d4b07c5ea757f2f6b35597b4a2" +source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" dependencies = [ "alloy", "alloy-primitives", @@ -6758,7 +6785,7 @@ dependencies = [ "taceo-eddsa-babyjubjub", "taceo-groth16-material", "taceo-groth16-sol", - "taceo-oprf", + "taceo-oprf 0.10.0", "taceo-poseidon2", "thiserror 2.0.18", "url", @@ -6768,8 +6795,7 @@ dependencies = [ [[package]] name = "world-id-proof" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091e4902d84a827900db7f030fc6bb3f9705e0abe6d20aca943148727bb625b8" +source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" dependencies = [ "ark-bn254", "ark-ec", @@ -6785,7 +6811,7 @@ dependencies = [ "taceo-circom-types", "taceo-eddsa-babyjubjub", "taceo-groth16-material", - "taceo-oprf", + "taceo-oprf 0.10.0", "taceo-poseidon2", "tar", "thiserror 2.0.18", diff --git a/Cargo.toml b/Cargo.toml index ea2552f4..230d906b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ alloy-core = { version = "1", default-features = false, features = [ ] } alloy-primitives = { version = "1", default-features = false } uniffi = { version = "0.31", features = ["tokio"] } -world-id-core = { version = "0.6", default-features = false } +world-id-core = { git = "https://github.com/worldcoin/world-id-protocol", rev = "8174a6a938b569005cd42ab22baa0c25091a3935", default-features = false } # internal walletkit-core = { version = "0.11.0", path = "walletkit-core", default-features = false } diff --git a/walletkit-core/src/authenticator/mod.rs b/walletkit-core/src/authenticator/mod.rs index 6988a42b..3145e120 100644 --- a/walletkit-core/src/authenticator/mod.rs +++ b/walletkit-core/src/authenticator/mod.rs @@ -276,7 +276,8 @@ impl Authenticator { Ok(self .inner .insert_authenticator(new_pubkey, new_address) - .await?) + .await? + .to_string()) } /// Updates an existing authenticator slot with a new authenticator. @@ -318,7 +319,8 @@ impl Authenticator { Ok(self .inner .update_authenticator(old_address, new_address, new_pubkey, index) - .await?) + .await? + .to_string()) } /// Removes an authenticator from the account. @@ -345,7 +347,11 @@ impl Authenticator { ) -> Result { let auth_address = Address::parse_from_ffi(&authenticator_address, "authenticator_address")?; - Ok(self.inner.remove_authenticator(auth_address, index).await?) + Ok(self + .inner + .remove_authenticator(auth_address, index) + .await? + .to_string()) } /// Polls the status of a gateway operation (insert, update, or remove authenticator). diff --git a/walletkit-core/src/credential.rs b/walletkit-core/src/credential.rs index e478c7e4..45fa753b 100644 --- a/walletkit-core/src/credential.rs +++ b/walletkit-core/src/credential.rs @@ -52,14 +52,16 @@ impl Credential { self.0.expires_at } - /// Returns the credential's `associated_data_hash` field element. + /// Returns the credential's associated-data commitment field element. /// - /// This is a Poseidon2 commitment to the associated data (e.g. a PCP archive) - /// set by the issuer at issuance time. Returns `FieldElement::ZERO` if no - /// associated data was committed to. + /// The upstream field was renamed from `associated_data_hash` to + /// `associated_data_commitment`; this accessor keeps the WalletKit FFI surface + /// stable while exposing the same value. It is a Poseidon2 commitment to the + /// associated data (e.g. a PCP archive) set by the issuer at issuance time. + /// Returns `FieldElement::ZERO` if no associated data was committed to. #[must_use] pub fn associated_data_hash(&self) -> FieldElement { - self.0.associated_data_hash.into() + self.0.associated_data_commitment.into() } } From 10f728e7c601c97ced19784470f37a8a6f3fe9d6 Mon Sep 17 00:00:00 2001 From: Otto Date: Wed, 25 Mar 2026 16:02:41 +0000 Subject: [PATCH 4/7] fix: resolve world-id-core 0.7 CI regressions --- walletkit-core/src/credential.rs | 2 +- walletkit-core/tests/proof_generation_integration.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/walletkit-core/src/credential.rs b/walletkit-core/src/credential.rs index 45fa753b..4b87de04 100644 --- a/walletkit-core/src/credential.rs +++ b/walletkit-core/src/credential.rs @@ -55,7 +55,7 @@ impl Credential { /// Returns the credential's associated-data commitment field element. /// /// The upstream field was renamed from `associated_data_hash` to - /// `associated_data_commitment`; this accessor keeps the WalletKit FFI surface + /// `associated_data_commitment`; this accessor keeps the `WalletKit` FFI surface /// stable while exposing the same value. It is a Poseidon2 commitment to the /// associated data (e.g. a PCP archive) set by the issuer at issuance time. /// Returns `FieldElement::ZERO` if no associated data was committed to. diff --git a/walletkit-core/tests/proof_generation_integration.rs b/walletkit-core/tests/proof_generation_integration.rs index def118da..1d9e3eb7 100644 --- a/walletkit-core/tests/proof_generation_integration.rs +++ b/walletkit-core/tests/proof_generation_integration.rs @@ -33,10 +33,9 @@ use alloy::signers::{local::PrivateKeySigner, SignerSync}; use alloy::sol; use alloy_primitives::U160; use eyre::{Context as _, Result}; -use taceo_oprf::types::OprfKeyId; use walletkit_core::storage::cache_embedded_groth16_material; use walletkit_core::{defaults::DefaultConfig, Authenticator, Environment}; -use world_id_core::primitives::{rp::RpId, FieldElement, Nullifier}; +use world_id_core::primitives::{rp::RpId, FieldElement, Nullifier, OprfKeyId}; use world_id_core::{ requests::{ProofRequest, RequestItem, RequestVersion}, Authenticator as CoreAuthenticator, EdDSAPrivateKey, From 46b637a3e52752f324eb0a4da0e4be2bf99b1388 Mon Sep 17 00:00:00 2001 From: Otto Date: Wed, 25 Mar 2026 16:55:59 +0000 Subject: [PATCH 5/7] refactor: review feedback (trim docs, delegate poll_status, 0.7.0, rename method) --- Cargo.lock | 20 +- Cargo.toml | 2 +- walletkit-core/src/authenticator/mod.rs | 234 ++---------------------- walletkit-core/src/credential.rs | 10 +- 4 files changed, 35 insertions(+), 231 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a84a8589..a637b115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6718,8 +6718,9 @@ dependencies = [ [[package]] name = "world-id-authenticator" -version = "0.6.0" -source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1713b3c70d6f2822f8ec0c12e313a74616fce2aa86c5cb54a991d13be5fe625" dependencies = [ "alloy", "anyhow", @@ -6747,8 +6748,9 @@ dependencies = [ [[package]] name = "world-id-core" -version = "0.6.0" -source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f0b7dab893d2588e13b3d3a0c6259467c7079b7cbe4a41db5a6bf4a04f46be" dependencies = [ "taceo-eddsa-babyjubjub", "world-id-authenticator", @@ -6758,8 +6760,9 @@ dependencies = [ [[package]] name = "world-id-primitives" -version = "0.6.0" -source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a38588112438f94966aef95559b48a0c3d4ae627c0ebc680ae4739ca0538c29" dependencies = [ "alloy", "alloy-primitives", @@ -6794,8 +6797,9 @@ dependencies = [ [[package]] name = "world-id-proof" -version = "0.6.0" -source = "git+https://github.com/worldcoin/world-id-protocol?rev=8174a6a938b569005cd42ab22baa0c25091a3935#8174a6a938b569005cd42ab22baa0c25091a3935" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0685fc6fcb0fef10a7caa80f7d4ac09727ee582934c2da39bdd82a423ab7abe" dependencies = [ "ark-bn254", "ark-ec", diff --git a/Cargo.toml b/Cargo.toml index 230d906b..88aea8d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ alloy-core = { version = "1", default-features = false, features = [ ] } alloy-primitives = { version = "1", default-features = false } uniffi = { version = "0.31", features = ["tokio"] } -world-id-core = { git = "https://github.com/worldcoin/world-id-protocol", rev = "8174a6a938b569005cd42ab22baa0c25091a3935", default-features = false } +world-id-core = { version = "0.7.0", default-features = false } # internal walletkit-core = { version = "0.11.0", path = "walletkit-core", default-features = false } diff --git a/walletkit-core/src/authenticator/mod.rs b/walletkit-core/src/authenticator/mod.rs index 3145e120..2d446fb2 100644 --- a/walletkit-core/src/authenticator/mod.rs +++ b/walletkit-core/src/authenticator/mod.rs @@ -8,7 +8,7 @@ use alloy_primitives::Address; use ruint_uniffi::Uint256; use std::sync::Arc; use world_id_core::{ - api_types::{GatewayErrorCode, GatewayRequestState, GatewayStatusResponse}, + api_types::{GatewayErrorCode, GatewayRequestState}, primitives::Config, Authenticator as CoreAuthenticator, Credential as CoreCredential, EdDSAPublicKey, InitializingAuthenticator as CoreInitializingAuthenticator, @@ -243,26 +243,10 @@ impl Authenticator { } /// Inserts a new authenticator to the account. - /// - /// The current authenticator signs the request to authorize adding a new authenticator. - /// The new authenticator will be registered in the `WorldIDRegistry` contract and can - /// subsequently sign operations on behalf of the same World ID. - /// - /// # Arguments - /// * `new_authenticator_pubkey_bytes` - The compressed off-chain `EdDSA` public key of the new - /// authenticator (32 bytes, little-endian). - /// * `new_authenticator_address` - The on-chain Ethereum address (hex string) of the new - /// authenticator. - /// - /// # Returns - /// A gateway request ID that can be used with [`poll_operation_status`](Self::poll_operation_status) - /// to track the on-chain finalization of the operation. - /// - /// # Errors - /// - Will error if the compressed public key bytes are invalid or not 32 bytes. - /// - Will error if the address string is not a valid hex address. - /// - Will error if there are network issues communicating with the indexer or gateway. - /// - Will error if the maximum number of authenticators has been reached. + #[allow( + clippy::missing_errors_doc, + reason = "FFI docs intentionally kept to a single summary line" + )] pub async fn insert_authenticator( &self, new_authenticator_pubkey_bytes: Vec, @@ -280,26 +264,11 @@ impl Authenticator { .to_string()) } - /// Updates an existing authenticator slot with a new authenticator. - /// - /// The current authenticator signs the request to authorize replacing the authenticator - /// at the specified slot index. - /// - /// # Arguments - /// * `old_authenticator_address` - The on-chain address (hex string) of the authenticator being replaced. - /// * `new_authenticator_address` - The on-chain address (hex string) of the new authenticator. - /// * `new_authenticator_pubkey_bytes` - The compressed off-chain `EdDSA` public key of the new - /// authenticator (32 bytes, little-endian). - /// * `index` - The pubkey slot index of the authenticator being replaced. - /// - /// # Returns - /// A gateway request ID that can be used with [`poll_operation_status`](Self::poll_operation_status). - /// - /// # Errors - /// - Will error if the compressed public key bytes are invalid. - /// - Will error if the address strings are not valid hex addresses. - /// - Will error if the index is out of bounds. - /// - Will error if there are network issues. + /// Updates an existing authenticator at the given slot index. + #[allow( + clippy::missing_errors_doc, + reason = "FFI docs intentionally kept to a single summary line" + )] pub async fn update_authenticator( &self, old_authenticator_address: String, @@ -323,23 +292,11 @@ impl Authenticator { .to_string()) } - /// Removes an authenticator from the account. - /// - /// The current authenticator signs the request to authorize removing the authenticator - /// at the specified slot index. An authenticator can remove itself or any other authenticator - /// on the same account. - /// - /// # Arguments - /// * `authenticator_address` - The on-chain address (hex string) of the authenticator to remove. - /// * `index` - The pubkey slot index of the authenticator being removed. - /// - /// # Returns - /// A gateway request ID that can be used with [`poll_operation_status`](Self::poll_operation_status). - /// - /// # Errors - /// - Will error if the address string is not a valid hex address. - /// - Will error if the index is out of bounds or there is no authenticator at that slot. - /// - Will error if there are network issues. + /// Removes an authenticator from the account at the given slot index. + #[allow( + clippy::missing_errors_doc, + reason = "FFI docs intentionally kept to a single summary line" + )] pub async fn remove_authenticator( &self, authenticator_address: String, @@ -366,25 +323,12 @@ impl Authenticator { &self, request_id: String, ) -> Result { - let url = format!("{}/status/{}", self.inner.config.gateway_url(), request_id); - let client = reqwest::Client::new(); // TODO: reuse client - let resp = client.get(&url).send().await?; - let status = resp.status(); + use world_id_core::api_types::GatewayRequestId; - if status.is_success() { - let body: GatewayStatusResponse = resp.json().await?; - Ok(body.status.into()) - } else { - let body = resp - .text() - .await - .unwrap_or_else(|e| format!("Unable to read response body: {e}")); - Err(WalletKitError::NetworkError { - url, - error: body, - status: Some(status.as_u16()), - }) - } + let raw = request_id.strip_prefix("gw_").unwrap_or(&request_id); + let id = GatewayRequestId::new(raw); + let status = self.inner.poll_status(&id).await?; + Ok(status.into()) } } @@ -812,142 +756,4 @@ mod storage_tests { drop(mock_server); cleanup_test_storage(&root); } - - #[tokio::test] - async fn test_poll_operation_status_finalized() { - let mut server = mockito::Server::new_async().await; - let mock = server - .mock("GET", "/status/req_abc") - .with_status(200) - .with_header("content-type", "application/json") - .with_body( - serde_json::json!({ - "request_id": "req_abc", - "kind": "insert_authenticator", - "status": { "state": "finalized", "tx_hash": "0x1234" } - }) - .to_string(), - ) - .create_async() - .await; - - // Create an Authenticator pointing at this mock server - let mut rpc_server = mockito::Server::new_async().await; - rpc_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 = [3u8; 32]; - let config = Config::new( - Some(rpc_server.url()), - 480, - address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"), - "https://unused-indexer.example.com".to_string(), - server.url(), - 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 auth = Authenticator::init(&seed, &config, &paths, Arc::new(store)) - .await - .unwrap(); - - let status = auth - .poll_operation_status("req_abc".to_string()) - .await - .unwrap(); - assert!(matches!(status, RegistrationStatus::Finalized)); - - mock.assert_async().await; - drop(server); - drop(rpc_server); - cleanup_test_storage(&root); - } - - #[tokio::test] - async fn test_poll_operation_status_gateway_error() { - let mut server = mockito::Server::new_async().await; - let mock = server - .mock("GET", "/status/req_bad") - .with_status(500) - .with_body("internal server error") - .create_async() - .await; - - let mut rpc_server = mockito::Server::new_async().await; - rpc_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 = [4u8; 32]; - let config = Config::new( - Some(rpc_server.url()), - 480, - address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"), - "https://unused-indexer.example.com".to_string(), - server.url(), - 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 auth = Authenticator::init(&seed, &config, &paths, Arc::new(store)) - .await - .unwrap(); - - let result = auth.poll_operation_status("req_bad".to_string()).await; - assert!(matches!( - result, - Err(WalletKitError::NetworkError { - status: Some(500), - .. - }) - )); - - mock.assert_async().await; - drop(server); - drop(rpc_server); - cleanup_test_storage(&root); - } } diff --git a/walletkit-core/src/credential.rs b/walletkit-core/src/credential.rs index 4b87de04..4b72c297 100644 --- a/walletkit-core/src/credential.rs +++ b/walletkit-core/src/credential.rs @@ -52,15 +52,9 @@ impl Credential { self.0.expires_at } - /// Returns the credential's associated-data commitment field element. - /// - /// The upstream field was renamed from `associated_data_hash` to - /// `associated_data_commitment`; this accessor keeps the `WalletKit` FFI surface - /// stable while exposing the same value. It is a Poseidon2 commitment to the - /// associated data (e.g. a PCP archive) set by the issuer at issuance time. - /// Returns `FieldElement::ZERO` if no associated data was committed to. + /// Returns the associated-data commitment field element for this credential. #[must_use] - pub fn associated_data_hash(&self) -> FieldElement { + pub fn associated_data_commitment(&self) -> FieldElement { self.0.associated_data_commitment.into() } } From ff914f922c0bb3b057d5869263eeff9ae4ffe809 Mon Sep 17 00:00:00 2001 From: Otto Date: Wed, 25 Mar 2026 17:08:31 +0000 Subject: [PATCH 6/7] docs: add concise error docs for authenticator ops --- walletkit-core/src/authenticator/mod.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/walletkit-core/src/authenticator/mod.rs b/walletkit-core/src/authenticator/mod.rs index 2d446fb2..34d33ffb 100644 --- a/walletkit-core/src/authenticator/mod.rs +++ b/walletkit-core/src/authenticator/mod.rs @@ -243,10 +243,9 @@ impl Authenticator { } /// Inserts a new authenticator to the account. - #[allow( - clippy::missing_errors_doc, - reason = "FFI docs intentionally kept to a single summary line" - )] + /// + /// # Errors + /// Returns an error if the pubkey bytes, address, or network call fails. pub async fn insert_authenticator( &self, new_authenticator_pubkey_bytes: Vec, @@ -265,10 +264,9 @@ impl Authenticator { } /// Updates an existing authenticator at the given slot index. - #[allow( - clippy::missing_errors_doc, - reason = "FFI docs intentionally kept to a single summary line" - )] + /// + /// # Errors + /// Returns an error if the address, pubkey bytes, index, or network call fails. pub async fn update_authenticator( &self, old_authenticator_address: String, @@ -293,10 +291,9 @@ impl Authenticator { } /// Removes an authenticator from the account at the given slot index. - #[allow( - clippy::missing_errors_doc, - reason = "FFI docs intentionally kept to a single summary line" - )] + /// + /// # Errors + /// Returns an error if the address or network call fails. pub async fn remove_authenticator( &self, authenticator_address: String, From b17d17542f2f34d833e0edb365b518723952095c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tr=C4=85d?= Date: Wed, 25 Mar 2026 18:40:44 +0100 Subject: [PATCH 7/7] Update Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 88aea8d1..71d9b0f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ alloy-core = { version = "1", default-features = false, features = [ ] } alloy-primitives = { version = "1", default-features = false } uniffi = { version = "0.31", features = ["tokio"] } -world-id-core = { version = "0.7.0", default-features = false } +world-id-core = { version = "0.7", default-features = false } # internal walletkit-core = { version = "0.11.0", path = "walletkit-core", default-features = false }