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