1use crate::{
18 identity_updates::IdentityContext,
19 identity_updates::{
20 IdentityUpdateError, IdentityUpdates, get_association_state_for_client,
21 get_association_state_with_verifier, load_identity_updates,
22 load_identity_updates_for_client, state_holds_identifier,
23 },
24};
25use futures::{StreamExt, TryStreamExt, stream};
26use std::collections::HashMap;
27use xmtp_api::{ApiClientWrapper, XmtpApi};
28use xmtp_common::{ErrorCode, RetryableError};
29use xmtp_configuration::IDENTITY_DEPENDENCY_CONCURRENCY;
30use xmtp_db::{NotFound, prelude::*};
31use xmtp_id::{
32 InboxId,
33 associations::{
34 AssociationError, AssociationState, Identifier, MAX_HANDLE_CLAIMS,
35 handle::{Handle, InvalidHandle},
36 },
37 scw_verifier::SmartContractSignatureVerifier,
38};
39use xmtp_proto::{types::ApiIdentifier, xmtp::identity::associations::IdentifierKind};
40
41#[cfg(test)]
42mod tests;
43
44pub const HANDLE_SEARCH_CODES: usize = 100;
47
48const HANDLE_CODE_COUNT: usize = 10_000;
50
51#[derive(Debug, thiserror::Error, ErrorCode)]
53pub enum HandleError {
54 #[error(transparent)]
55 #[error_code(inherit)]
56 Invalid(#[from] InvalidHandle),
57 #[error("no inbox holds the handle")]
61 NotFound,
62 #[error("the inbox has made the most handle claims it can")]
65 LimitReached,
66}
67
68impl RetryableError for HandleError {
69 fn is_retryable(&self) -> bool {
70 false
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum IdentityTarget {
78 Account(Identifier),
79 Handle(Handle),
80}
81
82impl IdentityTarget {
83 fn lookup(&self) -> ApiIdentifier {
85 match self {
86 Self::Account(identifier) => identifier.into(),
87 Self::Handle(handle) => ApiIdentifier {
88 identifier: handle.as_str().to_owned(),
89 identifier_kind: IdentifierKind::Handle,
90 },
91 }
92 }
93}
94
95pub fn handle_targets(handles: &[Handle]) -> Vec<IdentityTarget> {
97 handles
98 .iter()
99 .cloned()
100 .map(IdentityTarget::Handle)
101 .collect()
102}
103
104pub fn account_targets(accounts: &[Identifier]) -> Vec<IdentityTarget> {
106 accounts
107 .iter()
108 .cloned()
109 .map(IdentityTarget::Account)
110 .collect()
111}
112
113pub fn ensure_claim_allowed(state: &AssociationState) -> Result<(), HandleError> {
117 if state.handle_claims() >= MAX_HANDLE_CLAIMS {
118 return Err(HandleError::LimitReached);
119 }
120 Ok(())
121}
122
123async fn lookup_targets<A: XmtpApi>(
126 api: &ApiClientWrapper<A>,
127 targets: &[IdentityTarget],
128) -> Result<Vec<Option<InboxId>>, IdentityUpdateError> {
129 if targets.is_empty() {
130 return Ok(Vec::new());
131 }
132 Ok(api
133 .get_inbox_ids(targets.iter().map(IdentityTarget::lookup).collect())
134 .await?)
135}
136
137fn answered_inboxes(answers: &[Option<InboxId>]) -> Vec<InboxId> {
139 let mut inboxes: Vec<InboxId> = answers.iter().flatten().cloned().collect();
140 inboxes.sort_unstable();
141 inboxes.dedup();
142 inboxes
143}
144
145fn state_if_logged(
148 result: Result<AssociationState, IdentityUpdateError>,
149) -> Result<Option<AssociationState>, IdentityUpdateError> {
150 match result {
151 Ok(state) => Ok(Some(state)),
152 Err(IdentityUpdateError::Association(AssociationError::MissingIdentityUpdate)) => Ok(None),
153 Err(error) => Err(error),
154 }
155}
156
157fn state_holds(state: &AssociationState, target: &IdentityTarget) -> bool {
163 match target {
164 IdentityTarget::Handle(handle) => state.handles().contains(handle),
165 IdentityTarget::Account(identifier) => state_holds_identifier(state, identifier),
166 }
167}
168
169fn keep_verified(
173 targets: &[IdentityTarget],
174 answers: Vec<Option<InboxId>>,
175 states: &HashMap<InboxId, AssociationState>,
176) -> Vec<Option<InboxId>> {
177 targets
178 .iter()
179 .zip(answers)
180 .map(|(target, answer)| {
181 answer.filter(|inbox_id| {
182 states
183 .get(inbox_id)
184 .is_some_and(|state| state_holds(state, target))
185 })
186 })
187 .collect()
188}
189
190pub async fn resolve_verified_in<Context: IdentityContext>(
196 context: &Context,
197 targets: &[IdentityTarget],
198) -> Result<Vec<Option<InboxId>>, IdentityUpdateError> {
199 let answers = lookup_targets(context.api(), targets).await?;
200 let inboxes = answered_inboxes(&answers);
201 if inboxes.is_empty() {
202 return Ok(answers);
203 }
204 let conn = context.db();
205 let inbox_refs: Vec<&str> = inboxes.iter().map(String::as_str).collect();
206 load_identity_updates_for_client(context, &conn, &inbox_refs).await?;
207 let reads: Vec<_> = inboxes
210 .into_iter()
211 .map(|inbox_id| async {
212 let state = get_association_state_for_client(context, &conn, &inbox_id, None).await;
213 Ok::<_, IdentityUpdateError>(state_if_logged(state)?.map(|state| (inbox_id, state)))
214 })
215 .collect();
216 let states: Vec<_> = stream::iter(reads)
217 .buffered(IDENTITY_DEPENDENCY_CONCURRENCY)
218 .try_collect()
219 .await?;
220 let states: HashMap<InboxId, AssociationState> = states.into_iter().flatten().collect();
221 Ok(keep_verified(targets, answers, &states))
222}
223
224pub async fn resolve_verified_with_verifier<A: XmtpApi>(
227 api: &ApiClientWrapper<A>,
228 conn: &impl DbQuery,
229 targets: &[IdentityTarget],
230 verifier: &impl SmartContractSignatureVerifier,
231) -> Result<Vec<Option<InboxId>>, IdentityUpdateError> {
232 let answers = lookup_targets(api, targets).await?;
233 let inboxes = answered_inboxes(&answers);
234 if inboxes.is_empty() {
235 return Ok(answers);
236 }
237 let inbox_refs: Vec<&str> = inboxes.iter().map(String::as_str).collect();
238 load_identity_updates(api, conn, &inbox_refs).await?;
239 let mut states = HashMap::new();
240 for inbox_id in inboxes {
241 let state = get_association_state_with_verifier(conn, &inbox_id, None, verifier).await;
242 if let Some(state) = state_if_logged(state)? {
243 states.insert(inbox_id, state);
244 }
245 }
246 Ok(keep_verified(targets, answers, &states))
247}
248
249pub async fn resolve_targets_in<Context: IdentityContext>(
253 context: &Context,
254 targets: &[IdentityTarget],
255) -> Result<Vec<Option<InboxId>>, IdentityUpdateError> {
256 let inboxes = resolve_verified_in(context, targets).await?;
257 targets
258 .iter()
259 .zip(inboxes)
260 .map(|(target, inbox)| match (target, inbox) {
261 (IdentityTarget::Handle(_), None) => Err(HandleError::NotFound.into()),
262 (_, inbox) => Ok(inbox),
263 })
264 .collect()
265}
266
267fn search_candidates(handle: Handle) -> Vec<Handle> {
271 if handle.name() != handle.as_str() {
272 return vec![handle];
273 }
274 let codes = rand::seq::index::sample(
275 &mut xmtp_common::rng(),
276 HANDLE_CODE_COUNT,
277 HANDLE_SEARCH_CODES,
278 );
279 let coded: Vec<Handle> = codes
280 .into_iter()
281 .map(|code| {
282 let code = u16::try_from(code).expect("a sampled code is below 10 000");
283 Handle::with_code(handle.name(), code)
284 .expect("a name part with a four-digit code is a handle")
285 })
286 .collect();
287 std::iter::once(handle).chain(coded).collect()
288}
289
290pub async fn handles_have_owner<Context: IdentityContext>(
294 context: &Context,
295 handles: &[Handle],
296) -> Result<Vec<bool>, IdentityUpdateError> {
297 Ok(lookup_targets(context.api(), &handle_targets(handles))
298 .await?
299 .iter()
300 .map(Option::is_some)
301 .collect())
302}
303
304pub async fn available_handles<Context: IdentityContext>(
313 context: &Context,
314 input: &str,
315) -> Result<Vec<Handle>, IdentityUpdateError> {
316 let handle = Handle::from_input(input).map_err(HandleError::from)?;
317 let candidates = search_candidates(handle);
318 let owned = handles_have_owner(context, &candidates).await?;
319 Ok(candidates
320 .into_iter()
321 .zip(owned)
322 .filter_map(|(candidate, owned)| (!owned).then_some(candidate))
323 .collect())
324}
325
326pub async fn claim_handle<Context: IdentityContext>(
332 context: &Context,
333 handle: &Handle,
334) -> Result<(), IdentityUpdateError> {
335 let identity_updates = IdentityUpdates::new(context);
336 let conn = context.db();
337 load_identity_updates_for_client(context, &conn, &[context.inbox_id()]).await?;
338 let state = identity_updates
339 .get_association_state(&conn, context.inbox_id(), None)
340 .await?;
341 ensure_claim_allowed(&state)?;
342 let request = identity_updates
343 .claim_handle_request(handle.clone())
344 .await?;
345 identity_updates.apply_signature_request(request).await
346}
347
348pub async fn resolve_targets<Context: IdentityContext>(
352 context: &Context,
353 targets: &[IdentityTarget],
354) -> Result<Vec<InboxId>, IdentityUpdateError> {
355 let inboxes = resolve_targets_in(context, targets).await?;
356 targets
357 .iter()
358 .zip(inboxes)
359 .map(|(target, inbox)| match (target, inbox) {
360 (_, Some(inbox)) => Ok(inbox),
361 (IdentityTarget::Account(identifier), None) => {
362 Err(NotFound::InboxIdForAddress(identifier.to_string()).into())
363 }
364 (IdentityTarget::Handle(_), None) => Err(HandleError::NotFound.into()),
365 })
366 .collect()
367}