Skip to main content

world_id_authenticator/
recovery.rs

1use alloy::{primitives::Address, signers::Signature};
2use ruint::aliases::U256;
3
4use crate::{
5    api_types::{
6        CancelRecoveryAgentUpdateRequest, ExecuteRecoveryAgentUpdateRequest, GatewayRequestId,
7        GatewayStatusResponse, UpdateRecoveryAgentRequest,
8    },
9    authenticator::Authenticator,
10    error::AuthenticatorError,
11    registry::{domain, sign_cancel_recovery_agent_update, sign_initiate_recovery_agent_update},
12};
13
14impl Authenticator {
15    /// Initiates a recovery agent update for the holder's World ID.
16    ///
17    /// This begins a time-locked process to change the recovery agent. The update must be
18    /// executed after a cooldown period using [`execute_recovery_agent_update`](Self::execute_recovery_agent_update),
19    /// or it can be cancelled using [`cancel_recovery_agent_update`](Self::cancel_recovery_agent_update).
20    ///
21    /// # Errors
22    /// Returns an error if the gateway rejects the request or a network error occurs.
23    pub async fn initiate_recovery_agent_update(
24        &self,
25        new_recovery_agent: Address,
26    ) -> Result<GatewayRequestId, AuthenticatorError> {
27        let leaf_index = self.leaf_index();
28        let (sig, nonce) = self
29            .danger_sign_initiate_recovery_agent_update(new_recovery_agent)
30            .await?;
31
32        let req = UpdateRecoveryAgentRequest {
33            leaf_index,
34            new_recovery_agent,
35            signature: sig,
36            nonce,
37        };
38
39        let gateway_resp: GatewayStatusResponse = self
40            .gateway_client
41            .post_json(
42                self.config.gateway_url(),
43                "/initiate-recovery-agent-update",
44                &req,
45            )
46            .await?;
47        Ok(gateway_resp.request_id)
48    }
49
50    /// Signs the EIP-712 `InitiateRecoveryAgentUpdate` payload and returns the
51    /// signature without submitting anything to the gateway.
52    ///
53    /// This is the signing-only counterpart of [`Self::initiate_recovery_agent_update`].
54    /// Callers can use the returned signature to build and submit the gateway
55    /// request themselves.
56    ///
57    /// # Warning
58    /// This method uses the `onchain_signer` (secp256k1 ECDSA) and produces a
59    /// recoverable signature. Any holder of the signature together with the
60    /// EIP-712 parameters can call `ecrecover` to obtain the `onchain_address`,
61    /// which can then be looked up in the registry to derive the user's
62    /// `leaf_index`. Only expose the output to trusted parties (e.g. a Recovery
63    /// Agent).
64    ///
65    /// # Errors
66    /// Returns an error if the nonce fetch or signing step fails.
67    pub async fn danger_sign_initiate_recovery_agent_update(
68        &self,
69        new_recovery_agent: Address,
70    ) -> Result<(Signature, U256), AuthenticatorError> {
71        let leaf_index = self.leaf_index();
72        let nonce = self.signing_nonce().await?;
73        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
74
75        let signature = sign_initiate_recovery_agent_update(
76            &self.signer.onchain_signer(),
77            leaf_index,
78            new_recovery_agent,
79            nonce,
80            &eip712_domain,
81        )
82        .map_err(|e| {
83            AuthenticatorError::Generic(format!(
84                "Failed to sign initiate recovery agent update: {e}"
85            ))
86        })?;
87
88        Ok((signature, nonce))
89    }
90
91    /// Executes a pending recovery agent update for the holder's World ID.
92    ///
93    /// This is a permissionless operation that can be called by anyone after the cooldown
94    /// period has elapsed. No signature is required.
95    ///
96    /// # Errors
97    /// Returns an error if the gateway rejects the request or a network error occurs.
98    pub async fn execute_recovery_agent_update(
99        &self,
100    ) -> Result<GatewayRequestId, AuthenticatorError> {
101        let req = ExecuteRecoveryAgentUpdateRequest {
102            leaf_index: self.leaf_index(),
103        };
104
105        let gateway_resp: GatewayStatusResponse = self
106            .gateway_client
107            .post_json(
108                self.config.gateway_url(),
109                "/execute-recovery-agent-update",
110                &req,
111            )
112            .await?;
113        Ok(gateway_resp.request_id)
114    }
115
116    /// Cancels a pending recovery agent update for the holder's World ID.
117    ///
118    /// # Errors
119    /// Returns an error if the gateway rejects the request or a network error occurs.
120    pub async fn cancel_recovery_agent_update(
121        &self,
122    ) -> Result<GatewayRequestId, AuthenticatorError> {
123        let leaf_index = self.leaf_index();
124        let nonce = self.signing_nonce().await?;
125        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
126
127        let sig = sign_cancel_recovery_agent_update(
128            &self.signer.onchain_signer(),
129            leaf_index,
130            nonce,
131            &eip712_domain,
132        )
133        .map_err(|e| {
134            AuthenticatorError::Generic(format!("Failed to sign cancel recovery agent update: {e}"))
135        })?;
136
137        let req = CancelRecoveryAgentUpdateRequest {
138            leaf_index,
139            signature: sig,
140            nonce,
141        };
142
143        let gateway_resp: GatewayStatusResponse = self
144            .gateway_client
145            .post_json(
146                self.config.gateway_url(),
147                "/cancel-recovery-agent-update",
148                &req,
149            )
150            .await?;
151        Ok(gateway_resp.request_id)
152    }
153}