diff --git a/walletkit-cli/src/commands/recovery_binding.rs b/walletkit-cli/src/commands/recovery_binding.rs index e3263f3d..08ade7bc 100644 --- a/walletkit-cli/src/commands/recovery_binding.rs +++ b/walletkit-cli/src/commands/recovery_binding.rs @@ -3,6 +3,7 @@ use clap::Subcommand; use super::{init_authenticator, Cli}; +use crate::output; use walletkit_core::issuers::RecoveryBindingManager; use walletkit_core::Environment; @@ -15,6 +16,7 @@ pub enum RecoveryBindingCommand { UnregisterBindings { sub: String, }, + GetBinding, } pub async fn run( @@ -44,6 +46,25 @@ pub async fn run( .unbind_recovery_agent(&authenticator, sub.clone()) .await?; } + RecoveryBindingCommand::GetBinding => { + let recovery_binding_manager = + RecoveryBindingManager::new(environment).unwrap(); + let recovery_binding = recovery_binding_manager + .get_recovery_binding(authenticator.leaf_index()) + .await?; + if cli.json { + output::print_json_data( + &serde_json::json!({ + "recovery_agent": recovery_binding.recovery_agent, + "pending_recovery_agent": recovery_binding.pending_recovery_agent, + "execute_after": recovery_binding.execute_after, + }), + true, + ); + } else { + println!("Recovery binding: {recovery_binding:?}"); + } + } } Ok(()) } diff --git a/walletkit-core/src/issuers/pop_backend_client.rs b/walletkit-core/src/issuers/pop_backend_client.rs index 18847150..04f76575 100644 --- a/walletkit-core/src/issuers/pop_backend_client.rs +++ b/walletkit-core/src/issuers/pop_backend_client.rs @@ -30,7 +30,11 @@ struct ChallengeResponse { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, uniffi::Record)] pub struct RecoveryBindingResponse { #[serde(rename = "recoveryAgent")] - pub recovery_agent: String, + pub recovery_agent: Option, + #[serde(rename = "pendingRecoveryAgent")] + pub pending_recovery_agent: Option, + #[serde(rename = "executeAfter")] + pub execute_after: Option, } /// Low-level HTTP client for the Proof-of-Personhood (`PoP`) backend API. @@ -424,7 +428,10 @@ mod tests { let result = pop_api_client.get_recovery_binding(42).await; assert!(result.is_ok(), "Expected success but got error: {result:?}"); mock.assert_async().await; - assert_eq!(result.unwrap().recovery_agent, "0x1234567890abcdef"); + assert_eq!( + result.unwrap().recovery_agent, + Some("0x1234567890abcdef".to_string()) + ); drop(server); } @@ -452,4 +459,61 @@ mod tests { mock.assert_async().await; drop(server); } + + #[tokio::test] + async fn test_get_recovery_binding_no_pending_recovery_agent() { + let mut server = mockito::Server::new_async().await; + let url = server.url(); + + let mock = server + .mock("GET", "/api/v1/recovery-binding?leafIndex=42") + .with_status(200) + .with_body("{\"recoveryAgent\": \"0x1234567890abcdef\"}") + .create_async() + .await; + + let pop_api_client = PopBackendClient::new(url.clone()); + + let result = pop_api_client.get_recovery_binding(42).await; + assert!(result.is_ok(), "Expected success but got error: {result:?}"); + mock.assert_async().await; + let recovery_binding = result.unwrap(); + assert_eq!(recovery_binding.pending_recovery_agent, None); + assert_eq!(recovery_binding.execute_after, None); + assert_eq!( + recovery_binding.recovery_agent, + Some("0x1234567890abcdef".to_string()) + ); + drop(server); + } + + #[tokio::test] + async fn test_get_recovery_binding_with_pending_recovery_agent() { + let mut server = mockito::Server::new_async().await; + let url = server.url(); + + let mock = server + .mock("GET", "/api/v1/recovery-binding?leafIndex=42") + .with_status(200) + .with_body("{\"recoveryAgent\": \"0x0000000000000000000000000000000000000001\", \"pendingRecoveryAgent\": \"0x0000000000000000000000000000000000000000\", \"executeAfter\": \"0x01\"}") + .create_async() + .await; + + let pop_api_client = PopBackendClient::new(url.clone()); + + let result = pop_api_client.get_recovery_binding(42).await; + assert!(result.is_ok(), "Expected success but got error: {result:?}"); + mock.assert_async().await; + let recovery_binding = result.unwrap(); + assert_eq!( + recovery_binding.pending_recovery_agent, + Some("0x0000000000000000000000000000000000000000".to_string()) + ); + assert_eq!(recovery_binding.execute_after, Some("0x01".to_string())); + assert_eq!( + recovery_binding.recovery_agent, + Some("0x0000000000000000000000000000000000000001".to_string()) + ); + drop(server); + } } diff --git a/walletkit-core/src/issuers/recovery_bindings_manager.rs b/walletkit-core/src/issuers/recovery_bindings_manager.rs index 04c04925..6cbc90d6 100644 --- a/walletkit-core/src/issuers/recovery_bindings_manager.rs +++ b/walletkit-core/src/issuers/recovery_bindings_manager.rs @@ -15,11 +15,32 @@ use crate::authenticator::Authenticator; use crate::error::WalletKitError; use crate::issuers::pop_backend_client::ManageRecoveryBindingRequest; +use crate::issuers::pop_backend_client::RecoveryBindingResponse; use crate::issuers::PopBackendClient; use crate::Environment; use alloy_primitives::keccak256; use alloy_primitives::Address; use std::string::String; +/// Represents a recovery binding. +#[derive(Debug, PartialEq, Eq, uniffi::Record)] +pub struct RecoveryBinding { + /// The hex address of the recovery agent (e.g. `"0x1234…"`). + pub recovery_agent: Option, + /// The hex address of the pending recovery agent (e.g. `"0x1234…"`). + pub pending_recovery_agent: Option, + /// The timestamp of the recovery agent update in seconds since the Unix epoch. + pub execute_after: Option, +} + +impl From for RecoveryBinding { + fn from(response: RecoveryBindingResponse) -> Self { + Self { + recovery_agent: response.recovery_agent, + pending_recovery_agent: response.pending_recovery_agent, + execute_after: response.execute_after, + } + } +} /// Client for registering and unregistering recovery agents with the `PoP` backend. /// @@ -156,12 +177,12 @@ impl RecoveryBindingManager { pub async fn get_recovery_binding( &self, leaf_index: u64, - ) -> Result { + ) -> Result { let recovery_binding = self .pop_backend_client .get_recovery_binding(leaf_index) .await?; - Ok(recovery_binding.recovery_agent) + Ok(recovery_binding.into()) } }