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};
12use world_id_registries::world_id::{
13    domain, sign_cancel_recovery_agent_update, sign_initiate_recovery_agent_update,
14};
15
16impl Authenticator {
17    /// Initiates a recovery agent update for the holder's World ID.
18    ///
19    /// This begins a time-locked process to change the recovery agent. The update must be
20    /// executed after a cooldown period using [`execute_recovery_agent_update`](Self::execute_recovery_agent_update),
21    /// or it can be cancelled using [`cancel_recovery_agent_update`](Self::cancel_recovery_agent_update).
22    ///
23    /// # Errors
24    /// Returns an error if the gateway rejects the request or a network error occurs.
25    #[deprecated(
26        note = "WIP-102: use `update_recovery_agent`. The legacy URL still works against a V2-upgraded gateway, but the V2 contract changes the agent immediately (with a revert window) instead of starting a cooldown."
27    )]
28    pub async fn initiate_recovery_agent_update(
29        &self,
30        new_recovery_agent: Address,
31    ) -> Result<GatewayRequestId, AuthenticatorError> {
32        let leaf_index = self.leaf_index();
33        let (sig, nonce) = self
34            .danger_sign_initiate_recovery_agent_update(new_recovery_agent)
35            .await?;
36
37        let req = UpdateRecoveryAgentRequest {
38            leaf_index,
39            new_recovery_agent,
40            signature: sig,
41            nonce,
42        };
43
44        let gateway_resp: GatewayStatusResponse = self
45            .gateway_client
46            .post_json(
47                self.config.gateway_url(),
48                "/initiate-recovery-agent-update",
49                &req,
50            )
51            .await?;
52        Ok(gateway_resp.request_id)
53    }
54
55    /// Signs the EIP-712 `InitiateRecoveryAgentUpdate` payload and returns the
56    /// signature without submitting anything to the gateway.
57    ///
58    /// This is the signing-only counterpart of [`Self::initiate_recovery_agent_update`].
59    /// Callers can use the returned signature to build and submit the gateway
60    /// request themselves.
61    ///
62    /// # Warning
63    /// This method uses the `onchain_signer` (secp256k1 ECDSA) and produces a
64    /// recoverable signature. Any holder of the signature together with the
65    /// EIP-712 parameters can call `ecrecover` to obtain the `onchain_address`,
66    /// which can then be looked up in the registry to derive the user's
67    /// `leaf_index`. Only expose the output to trusted parties (e.g. a Recovery
68    /// Agent).
69    ///
70    /// # Errors
71    /// Returns an error if the nonce fetch or signing step fails.
72    pub async fn danger_sign_initiate_recovery_agent_update(
73        &self,
74        new_recovery_agent: Address,
75    ) -> Result<(Signature, U256), AuthenticatorError> {
76        let leaf_index = self.leaf_index();
77        let nonce = self.signing_nonce().await?;
78        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
79
80        let signature = sign_initiate_recovery_agent_update(
81            &self.signer.onchain_signer(),
82            leaf_index,
83            new_recovery_agent,
84            nonce,
85            &eip712_domain,
86        )
87        .map_err(|e| {
88            AuthenticatorError::Generic(format!(
89                "Failed to sign initiate recovery agent update: {e}"
90            ))
91        })?;
92
93        Ok((signature, nonce))
94    }
95
96    /// Updates the holder's recovery agent (WIP-102).
97    ///
98    /// On a V2 registry the new agent becomes effective immediately, but for a
99    /// revert window (`getRecoveryAgentUpdateCooldown` seconds) any
100    /// authenticator can call [`Self::revert_recovery_agent_update`] to roll
101    /// back. During that window the *previous* agent remains the only valid
102    /// signer for `recoverAccount`, which mitigates a compromised authenticator
103    /// silently swapping in an attacker-controlled recovery address.
104    ///
105    /// # Errors
106    /// Returns an error if the gateway rejects the request or a network error occurs.
107    pub async fn update_recovery_agent(
108        &self,
109        new_recovery_agent: Address,
110    ) -> Result<GatewayRequestId, AuthenticatorError> {
111        let leaf_index = self.leaf_index();
112        let (sig, nonce) = self
113            .danger_sign_initiate_recovery_agent_update(new_recovery_agent)
114            .await?;
115
116        let req = UpdateRecoveryAgentRequest {
117            leaf_index,
118            new_recovery_agent,
119            signature: sig,
120            nonce,
121        };
122
123        let gateway_resp: GatewayStatusResponse = self
124            .gateway_client
125            .post_json(self.config.gateway_url(), "/update-recovery-agent", &req)
126            .await?;
127        Ok(gateway_resp.request_id)
128    }
129
130    /// Executes a pending recovery agent update for the holder's World ID.
131    ///
132    /// This is a permissionless operation that can be called by anyone after the cooldown
133    /// period has elapsed. No signature is required.
134    ///
135    /// # Errors
136    /// Returns an error if the gateway rejects the request or a network error occurs.
137    #[deprecated(
138        note = "WIP-102: this operation no longer exists. On a V2-upgraded gateway the call is a no-op (returns Finalized without touching chain). Remove the call from your flow."
139    )]
140    pub async fn execute_recovery_agent_update(
141        &self,
142    ) -> Result<GatewayRequestId, AuthenticatorError> {
143        let req = ExecuteRecoveryAgentUpdateRequest {
144            leaf_index: self.leaf_index(),
145        };
146
147        let gateway_resp: GatewayStatusResponse = self
148            .gateway_client
149            .post_json(
150                self.config.gateway_url(),
151                "/execute-recovery-agent-update",
152                &req,
153            )
154            .await?;
155        Ok(gateway_resp.request_id)
156    }
157
158    /// Cancels a pending recovery agent update for the holder's World ID.
159    ///
160    /// # Errors
161    /// Returns an error if the gateway rejects the request or a network error occurs.
162    #[deprecated(
163        note = "WIP-102: use `revert_recovery_agent_update`. The legacy URL still works against a V2-upgraded gateway, but the new method name reflects WIP-102 semantics: the operation can only succeed within the revert window after `update_recovery_agent`."
164    )]
165    pub async fn cancel_recovery_agent_update(
166        &self,
167    ) -> Result<GatewayRequestId, AuthenticatorError> {
168        let leaf_index = self.leaf_index();
169        let nonce = self.signing_nonce().await?;
170        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
171
172        let sig = sign_cancel_recovery_agent_update(
173            &self.signer.onchain_signer(),
174            leaf_index,
175            nonce,
176            &eip712_domain,
177        )
178        .map_err(|e| {
179            AuthenticatorError::Generic(format!("Failed to sign cancel recovery agent update: {e}"))
180        })?;
181
182        let req = CancelRecoveryAgentUpdateRequest {
183            leaf_index,
184            signature: sig,
185            nonce,
186        };
187
188        let gateway_resp: GatewayStatusResponse = self
189            .gateway_client
190            .post_json(
191                self.config.gateway_url(),
192                "/cancel-recovery-agent-update",
193                &req,
194            )
195            .await?;
196        Ok(gateway_resp.request_id)
197    }
198
199    /// Reverts an in-flight recovery agent update during the revert window (WIP-102).
200    ///
201    /// # Errors
202    /// Returns an error if the gateway rejects the request or a network error occurs.
203    pub async fn revert_recovery_agent_update(
204        &self,
205    ) -> Result<GatewayRequestId, AuthenticatorError> {
206        let leaf_index = self.leaf_index();
207        let nonce = self.signing_nonce().await?;
208        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
209
210        let sig = sign_cancel_recovery_agent_update(
211            &self.signer.onchain_signer(),
212            leaf_index,
213            nonce,
214            &eip712_domain,
215        )
216        .map_err(|e| {
217            AuthenticatorError::Generic(format!("Failed to sign revert recovery agent update: {e}"))
218        })?;
219
220        let req = CancelRecoveryAgentUpdateRequest {
221            leaf_index,
222            signature: sig,
223            nonce,
224        };
225
226        let gateway_resp: GatewayStatusResponse = self
227            .gateway_client
228            .post_json(
229                self.config.gateway_url(),
230                "/revert-recovery-agent-update",
231                &req,
232            )
233            .await?;
234        Ok(gateway_resp.request_id)
235    }
236}