diff --git a/walletkit-cli/src/commands/auth.rs b/walletkit-cli/src/commands/auth.rs index 1e8f12ba..81a2b15d 100644 --- a/walletkit-cli/src/commands/auth.rs +++ b/walletkit-cli/src/commands/auth.rs @@ -3,7 +3,7 @@ use clap::Subcommand; use eyre::WrapErr as _; use walletkit_core::error::WalletKitError; -use walletkit_core::{InitializingAuthenticator, RecoveryData, RegistrationStatus}; +use walletkit_core::{GatewayRequestStatus, InitializingAuthenticator, RecoveryData}; use crate::output; @@ -61,8 +61,10 @@ pub async fn register_and_poll( let status = init_auth.poll_status().await.wrap_err("poll failed")?; match &status { - RegistrationStatus::Finalized => return Ok(RegisterOutcome::Finalized), - RegistrationStatus::Failed { error, error_code } => { + GatewayRequestStatus::Finalized { .. } => { + return Ok(RegisterOutcome::Finalized) + } + GatewayRequestStatus::Failed { error, error_code } => { eyre::bail!("registration failed: {error} (code: {error_code:?})"); } _ => { diff --git a/walletkit-core/src/authenticator/mod.rs b/walletkit-core/src/authenticator/mod.rs index 574b25e6..0f7f0299 100644 --- a/walletkit-core/src/authenticator/mod.rs +++ b/walletkit-core/src/authenticator/mod.rs @@ -9,10 +9,10 @@ use ruint::aliases::U256; use ruint_uniffi::Uint256; use std::sync::Arc; use world_id_core::{ - api_types::{GatewayErrorCode, GatewayRequestState}, + api_types::{GatewayErrorCode, GatewayRequestId, GatewayRequestState}, primitives::{AuthenticatorPublicKeySet, Config}, Authenticator as CoreAuthenticator, AuthenticatorConfig, - Credential as CoreCredential, CredentialInput, + Credential as CoreCredential, CredentialInput, EdDSAPublicKey, InitializingAuthenticator as CoreInitializingAuthenticator, OnchainKeyRepresentable, Signer, }; @@ -339,6 +339,119 @@ impl Authenticator { Ok(request_id.to_string()) } + + /// Inserts a new authenticator into this account. + /// + /// Accepts the new authenticator's compressed EdDSA public key as a U256 and its on-chain signer + /// address as a hex string. Returns a gateway request ID; poll it with `poll_gateway_request_status` + /// to wait for finalization before initializing the new authenticator. + /// + /// # Errors + /// Returns [`WalletKitError::InvalidInput`] if the public key or address cannot be parsed. + /// Returns a network or authenticator error if the gateway request fails. + pub async fn insert_authenticator( + &self, + new_authenticator_pubkey: Uint256, + new_authenticator_address: String, + ) -> Result { + let new_authenticator_pubkey = eddsa_public_key_from_uint256( + 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()) + } + + /// Updates an existing authenticator slot. + /// + /// `pubkey_id` identifies the slot to replace; supply the old signer address, the new signer address, + /// and the new compressed EdDSA public key as a U256. Returns a gateway request ID; poll it with + /// `poll_gateway_request_status` to wait for finalization. + /// + /// # Errors + /// Returns [`WalletKitError::InvalidInput`] if any address or the public key cannot be parsed. + /// Returns a network or authenticator error if the gateway request fails. + pub async fn update_authenticator( + &self, + old_authenticator_address: String, + new_authenticator_address: String, + new_authenticator_pubkey: Uint256, + pubkey_id: u32, + ) -> Result { + let old_authenticator_address = Address::parse_from_ffi( + &old_authenticator_address, + "old_authenticator_address", + )?; + let new_authenticator_address = Address::parse_from_ffi( + &new_authenticator_address, + "new_authenticator_address", + )?; + let new_authenticator_pubkey = eddsa_public_key_from_uint256( + new_authenticator_pubkey, + "new_authenticator_pubkey", + )?; + + let request_id = self + .inner + .update_authenticator( + old_authenticator_address, + new_authenticator_address, + new_authenticator_pubkey, + pubkey_id, + ) + .await?; + + Ok(request_id.to_string()) + } + + /// Removes an authenticator from this account. + /// + /// `authenticator_address` and `pubkey_id` together identify the slot to remove. Returns a gateway + /// request ID; poll it with `poll_gateway_request_status` to wait for finalization. + /// + /// # Errors + /// Returns [`WalletKitError::InvalidInput`] if the authenticator address cannot be parsed. + /// Returns a network or authenticator error if the gateway request fails. + pub async fn remove_authenticator( + &self, + authenticator_address: String, + pubkey_id: u32, + ) -> Result { + let authenticator_address = + Address::parse_from_ffi(&authenticator_address, "authenticator_address")?; + + let request_id = self + .inner + .remove_authenticator(authenticator_address, pubkey_id) + .await?; + + Ok(request_id.to_string()) + } + + /// Polls the gateway for the status of a previously submitted request. + /// + /// Accepts a request ID returned by any authenticator-management or recovery method. Returns a + /// `GatewayRequestStatus` indicating whether the request is queued, submitted, finalized, or failed. + /// + /// # Errors + /// Returns a network error if the gateway cannot be reached or returns an unexpected response. + pub async fn poll_gateway_request_status( + &self, + request_id: String, + ) -> Result { + let request_id = gateway_request_id_from_string(&request_id); + let status = self.inner.poll_status(&request_id).await?; + Ok(status.into()) + } } #[uniffi::export(async_runtime = "tokio")] @@ -593,15 +706,21 @@ impl Authenticator { /// Registration status for a World ID being created through the gateway. #[derive(Debug, Clone, uniffi::Enum)] -pub enum RegistrationStatus { +pub enum GatewayRequestStatus { /// Request queued but not yet batched. Queued, /// Request currently being batched. Batching, /// Request submitted on-chain. - Submitted, - /// Request finalized on-chain. The World ID is now registered. - Finalized, + Submitted { + /// Transaction hash emitted when the request was submitted. + tx_hash: String, + }, + /// Request finalized on-chain. + Finalized { + /// Transaction hash emitted when the request was finalized. + tx_hash: String, + }, /// Request failed during processing. Failed { /// Error message returned by the gateway. @@ -611,13 +730,13 @@ pub enum RegistrationStatus { }, } -impl From for RegistrationStatus { +impl From for GatewayRequestStatus { fn from(state: GatewayRequestState) -> Self { match state { GatewayRequestState::Queued => Self::Queued, GatewayRequestState::Batching => Self::Batching, - GatewayRequestState::Submitted { .. } => Self::Submitted, - GatewayRequestState::Finalized { .. } => Self::Finalized, + 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(|c: GatewayErrorCode| c.to_string()), @@ -648,6 +767,7 @@ impl InitializingAuthenticator { name = "gateway_register", skip_all )] + #[expect(clippy::missing_errors_doc, reason = "FFI")] pub async fn register_with_defaults( seed: &[u8], rpc_url: Option, @@ -680,6 +800,7 @@ impl InitializingAuthenticator { name = "gateway_register", skip_all )] + #[expect(clippy::missing_errors_doc, reason = "FFI")] pub async fn register( seed: &[u8], config: &str, @@ -710,12 +831,30 @@ impl InitializingAuthenticator { name = "gateway_poll", skip_all )] - pub async fn poll_status(&self) -> Result { + #[expect(clippy::missing_errors_doc, reason = "FFI")] + pub async fn poll_status(&self) -> Result { let status = self.0.poll_status().await?; Ok(status.into()) } } +fn eddsa_public_key_from_uint256( + public_key: Uint256, + attribute: &'static str, +) -> Result { + let public_key: U256 = public_key.into(); + EdDSAPublicKey::from_compressed_bytes(public_key.to_le_bytes()).map_err(|error| { + WalletKitError::InvalidInput { + attribute: attribute.to_string(), + reason: error.to_string(), + } + }) +} + +fn gateway_request_id_from_string(request_id: &str) -> GatewayRequestId { + GatewayRequestId::new(request_id.strip_prefix("gw_").unwrap_or(request_id)) +} + /// The signature and signing nonce returned by /// [`Authenticator::danger_sign_initiate_recovery_agent_update`]. /// @@ -789,10 +928,13 @@ pub fn recovery_data_from_seed(seed: &[u8]) -> Result